Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/backend/src/controllers/calendar.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ export default class CalendarController {
questionDocumentLink,
location,
zoomLink,
description
description,
mention
} = req.body;

const parsedScheduleSlots = scheduleSlots.map((slot: any) => ({
Expand Down Expand Up @@ -307,7 +308,8 @@ export default class CalendarController {
questionDocumentLink,
location,
zoomLink,
description
description,
mention
);
res.status(200).json(event);
} catch (error: unknown) {
Expand All @@ -333,7 +335,8 @@ export default class CalendarController {
questionDocumentLink,
location,
zoomLink,
description
description,
mention
} = req.body;

const event = await CalendarService.editEvent(
Expand All @@ -353,7 +356,8 @@ export default class CalendarController {
questionDocumentLink,
location,
zoomLink,
description
description,
mention
);
res.status(200).json(event);
} catch (error: unknown) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "Slack_Mention_Type" AS ENUM ('USER', 'CHANNEL');

-- AlterTable
ALTER TABLE "Event" ADD COLUMN "mention" "Slack_Mention_Type" NOT NULL DEFAULT 'USER';
6 changes: 6 additions & 0 deletions src/backend/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ enum Event_Status {
DONE
}

enum Slack_Mention_Type {
USER
CHANNEL
}

enum Graph_Type {
PROJECT_BUDGET_BY_PROJECT
PROJECT_BUDGET_BY_TEAM
Expand Down Expand Up @@ -1181,6 +1186,7 @@ model Event {
description String?
notificationSlackThreads Message_Info[]
calendarEventIds String[]
mention Slack_Mention_Type @default(USER)
}

model Calendar {
Expand Down
22 changes: 15 additions & 7 deletions src/backend/src/services/calendar.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
ScheduleSlot,
notGuest,
isSameDay,
EventInstance
EventInstance,
SlackMentionType
} from 'shared';
import { getCalendarQueryArgs } from '../prisma-query-args/calendar.query-args.js';
import { getEventTypeQueryArgs } from '../prisma-query-args/event-type.query-args.js';
Expand Down Expand Up @@ -67,7 +68,7 @@ import {
updateUserAvailability,
areUsersinList
} from '../utils/users.utils.js';
import { Conflict_Status, Event_Status, Organization, Team } from '@prisma/client';
import { Conflict_Status, Event_Status, Organization, Team, Slack_Mention_Type } from '@prisma/client';

export default class CalendarService {
/**
Expand Down Expand Up @@ -270,7 +271,8 @@ export default class CalendarService {
questionDocumentLink?: string,
location?: string,
zoomLink?: string,
description?: string
description?: string,
mention?: SlackMentionType
): Promise<Event> {
// Validate eventTypeId
const foundEventType = await prisma.event_Type.findUnique({
Expand Down Expand Up @@ -461,7 +463,8 @@ export default class CalendarService {
location,
zoomLink,
questionDocumentLink,
description
description,
mention: mention === SlackMentionType.CHANNEL ? Slack_Mention_Type.CHANNEL : Slack_Mention_Type.USER
},
...getEventQueryArgs(organization.organizationId)
});
Expand Down Expand Up @@ -541,7 +544,8 @@ export default class CalendarService {
createdEvent,
submitter,
workPackageNames,
organization.name
organization.name,
{ memberSlackIds: memberUserSettings.map((s) => s.slackId).filter((id): id is string => !!id) }
);
}

Expand Down Expand Up @@ -591,7 +595,8 @@ export default class CalendarService {
questionDocumentLink?: string,
location?: string,
zoomLink?: string,
description?: string
description?: string,
mention?: SlackMentionType
): Promise<Event> {
// validate eventId
const foundEvent = await prisma.event.findUnique({
Expand Down Expand Up @@ -774,7 +779,10 @@ export default class CalendarService {
location,
zoomLink,
questionDocumentLink,
description
description,
...(mention !== undefined && {
mention: mention === SlackMentionType.CHANNEL ? Slack_Mention_Type.CHANNEL : Slack_Mention_Type.USER
})
},
...getEventQueryArgs(organization.organizationId)
});
Expand Down
20 changes: 16 additions & 4 deletions src/backend/src/transformers/calendar.transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
Prisma,
DayOfWeek as PrismaDayOfWeek,
Event_Status as PrismaEventStatus,
Conflict_Status as PrismaConflictStatus
Conflict_Status as PrismaConflictStatus,
Slack_Mention_Type as PrismaSlackMentionType
} from '@prisma/client';
import {
Machinery,
Expand All @@ -17,7 +18,8 @@ import {
DayOfWeek,
ConflictStatus,
Document,
EventWithMembers
EventWithMembers,
SlackMentionType
} from 'shared';
import { CalendarQueryArgs } from '../prisma-query-args/calendar.query-args.js';
import { EventTypeQueryArgs } from '../prisma-query-args/event-type.query-args.js';
Expand Down Expand Up @@ -134,7 +136,8 @@ export const eventTransformer = (event: Prisma.EventGetPayload<EventQueryArgs>):
questionDocumentLink: event.questionDocumentLink ?? undefined,
description: event.description ?? undefined,
status: eventStatusTransformer(event.status),
initialDateScheduled: event.initialDateScheduled ?? undefined
initialDateScheduled: event.initialDateScheduled ?? undefined,
mention: slackMentionTypeTransformer(event.mention)
};
};

Expand Down Expand Up @@ -178,7 +181,8 @@ export const eventWithMembersTransformer = (event: Prisma.EventGetPayload<EventW
questionDocumentLink: event.questionDocumentLink ?? undefined,
description: event.description ?? undefined,
status: eventStatusTransformer(event.status),
initialDateScheduled: event.initialDateScheduled ?? undefined
initialDateScheduled: event.initialDateScheduled ?? undefined,
mention: slackMentionTypeTransformer(event.mention)
};
};

Expand Down Expand Up @@ -231,3 +235,11 @@ export const eventStatusTransformer = (status: PrismaEventStatus): EventStatus =
};
return mapping[status];
};

export const slackMentionTypeTransformer = (mention: PrismaSlackMentionType): SlackMentionType => {
const mapping: Record<PrismaSlackMentionType, SlackMentionType> = {
USER: SlackMentionType.USER,
CHANNEL: SlackMentionType.CHANNEL
};
return mapping[mention];
};
33 changes: 26 additions & 7 deletions src/backend/src/utils/slack.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
CreateSponsorTask,
User,
Event,
formatForSlack
formatForSlack,
SlackMentionType
} from 'shared';
import { Account_Code, Reimbursement_Product_Other_Reason, Sponsor_Task } from '@prisma/client';
import {
Expand Down Expand Up @@ -383,35 +384,48 @@ export const sendAndGetSlackCRNotifications = async (
return notifications;
};

export const buildSlackMentionPrefix = (mention: SlackMentionType, memberSlackIds: string[]): string => {
if (mention === SlackMentionType.CHANNEL) return '<!channel> ';
if (memberSlackIds.length > 0) return `${memberSlackIds.map((id) => `<@${id}>`).join(' ')} `;
return '';
};

export const sendSlackEventNotification = async (
team: Team,
message: string
): Promise<{ channelId: string; ts: string }[]> => {
if (process.env.NODE_ENV !== 'production' && !DEV_TESTING_OVERRIDE) return []; // don't send msgs unless in prod
const msgs: { channelId: string; ts: string }[] = [];
const fullMsg = `${message}`;
const fullLink = `https://finishlinebyner.com/calendar`;
const btnText = `View Calendar`;
const notification = await sendMessage(team.slackId, fullMsg, fullLink, btnText);
const notification = await sendMessage(team.slackId, message, fullLink, btnText);
if (notification) msgs.push(notification);

return msgs;
};

export interface EventNotificationOptions {
memberSlackIds?: string[];
}

export const sendSlackEventNotifications = async (
teams: Team[],
event: Event,
submitter: User,
workPackageName: string,
projectName: string
projectName: string,
options: EventNotificationOptions = {}
) => {
if (process.env.NODE_ENV !== 'production' && !DEV_TESTING_OVERRIDE) return []; // don't send msgs unless in prod
const notifications: { channelId: string; ts: string }[] = [];

const mentionPrefix = buildSlackMentionPrefix(event.mention, options.memberSlackIds ?? []);

let message;
if (workPackageName) {
message = `:spiral_calendar_pad: ${event.title} for *${workPackageName}* is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
message = `${mentionPrefix}:spiral_calendar_pad: ${event.title} for *${workPackageName}* is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
} else {
message = `:spiral_calendar_pad: ${event.title} is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
message = `${mentionPrefix}:spiral_calendar_pad: ${event.title} is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
}

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

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

const allMembers = [...event.requiredMembers, ...event.optionalMembers];
const resolvedSlackIds = await Promise.all(allMembers.map((m) => getUserSlackId(m.userId)));
const validSlackIds = resolvedSlackIds.filter((id): id is string => !!id);
const mentionPrefix = buildSlackMentionPrefix(event.mention, validSlackIds);

const msg = `:spiral_calendar_pad: ${event.title} for *${drName}* has been scheduled for *${drTime}* ${location} by ${drSubmitter}`;
const docLink = event.questionDocumentLink ? `<${event.questionDocumentLink}|Doc Link>` : '';
const threadMsg = `This event has been Scheduled! \n` + docLink;
const threadMsg = `${mentionPrefix}This event has been Scheduled! \n` + docLink;

if (threads && threads.length !== 0) {
const msgs = threads.map((thread) => editMessage(thread.channelId, thread.timestamp, msg));
Expand Down
95 changes: 94 additions & 1 deletion src/backend/tests/unit/calendar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../test-data/users.test-data';
import { createTestOrganization, createTestUser, resetUsers } from '../test-utils';
import prisma from '../../src/prisma/prisma';
import { EventType, Machinery, ScheduleSlotCreateArgs, Shop, Event } from 'shared';
import { EventType, Machinery, ScheduleSlotCreateArgs, Shop, Event, SlackMentionType } from 'shared';

describe('Calendar Tests', () => {
let orgId: string;
Expand Down Expand Up @@ -1266,6 +1266,71 @@ describe('Calendar Tests', () => {
)
).rejects.toThrow(new NotFoundException('Machinery', deletedMachinery.machineryId));
});

it('defaults mention to USER when not specified', async () => {
const scheduleSlots = [
{
startTime: new Date('2025-10-13T09:00:00Z'),
endTime: new Date('2025-10-13T10:00:00Z'),
allDay: false
}
];

const result = await CalendarService.createEvent(
adminUser,
'User Mention Event',
eventType.eventTypeId,
organization,
[member.userId],
[],
[],
[shop.shopId],
[machinery.machineryId],
[],
scheduleSlots,
undefined, // initialDateScheduled
undefined, // teamTypeId
undefined, // questionDocumentLink
'Conference Room A',
undefined, // zoomLink
'Test description'
);

expect(result.mention).toBe(SlackMentionType.USER);
});

it('persists CHANNEL mention type when specified', async () => {
const scheduleSlots = [
{
startTime: new Date('2025-10-13T09:00:00Z'),
endTime: new Date('2025-10-13T10:00:00Z'),
allDay: false
}
];

const result = await CalendarService.createEvent(
adminUser,
'Channel Mention Event',
eventType.eventTypeId,
organization,
[member.userId],
[],
[],
[shop.shopId],
[machinery.machineryId],
[],
scheduleSlots,
undefined, // initialDateScheduled
undefined, // teamTypeId
undefined, // questionDocumentLink
'Conference Room A',
undefined, // zoomLink
'Test description',
SlackMentionType.CHANNEL
);

expect(result.mention).toBe(SlackMentionType.CHANNEL);
});
});

describe('Get Events', () => {
Expand Down Expand Up @@ -1899,6 +1964,34 @@ describe('Calendar Tests', () => {
expect(result.zoomLink).toBe('https://zoom.us/updated');
expect(result.description).toBe('Updated description');
});

it('updates mention from USER to CHANNEL', async () => {
// event is created with default USER mention in beforeEach
expect(event.mention).toBe(SlackMentionType.USER);

const result = await CalendarService.editEvent(
adminUser,
event.eventId,
event.title,
organization,
[member.userId],
[adminUser.userId],
Event_Status.SCHEDULED,
[],
[shop.shopId],
[machinery.machineryId],
[],
[],
undefined,
undefined,
undefined,
undefined,
undefined,
SlackMentionType.CHANNEL
);

expect(result.mention).toBe(SlackMentionType.CHANNEL);
});
});

describe('Delete Event', () => {
Expand Down
Loading
Loading