From 2f337d9a9da3fd31fd980760e6a5bddb4734de22 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 17 Dec 2024 23:50:44 -0500 Subject: [PATCH 01/17] #3074-created get unread announcements endpoint --- .../src/controllers/users.controllers.ts | 12 ++++++++++++ .../announcements.query.args.ts | 12 ++++++++++++ .../migration.sql | 4 +++- src/backend/src/prisma/schema.prisma | 16 +++++++++------- src/backend/src/routes/users.routes.ts | 1 + src/backend/src/services/users.services.ts | 18 ++++++++++++++++++ .../transformers/announcements.transformer.ts | 17 +++++++++++++++++ src/shared/index.ts | 1 + src/shared/src/types/announcements.types.ts | 10 ++++++++++ 9 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 src/backend/src/prisma-query-args/announcements.query.args.ts rename src/backend/src/prisma/migrations/{20241218031222_home_page_updates => 20241218044032_homepage_updates}/migration.sql (95%) create mode 100644 src/backend/src/transformers/announcements.transformer.ts create mode 100644 src/shared/src/types/announcements.types.ts diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index bf965c8d8e..cc084c714e 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -216,4 +216,16 @@ export default class UsersController { next(error); } } + + static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) { + try { + const { userId } = req.params; + const { organization } = req; + + const unreadAnnouncements = await UsersService.getUserUnreadAnnouncements(userId, organization); + res.status(200).json(unreadAnnouncements); + } catch (error: unknown) { + next(error); + } + } } diff --git a/src/backend/src/prisma-query-args/announcements.query.args.ts b/src/backend/src/prisma-query-args/announcements.query.args.ts new file mode 100644 index 0000000000..2f0e1ba294 --- /dev/null +++ b/src/backend/src/prisma-query-args/announcements.query.args.ts @@ -0,0 +1,12 @@ +import { Prisma } from '@prisma/client'; +import { getUserQueryArgs } from './user.query-args'; + +export type AnnouncementQueryArgs = ReturnType; + +export const getAnnouncementQueryArgs = (organizationId: string) => + Prisma.validator()({ + include: { + usersReceived: getUserQueryArgs(organizationId), + userCreated: getUserQueryArgs(organizationId) + } + }); diff --git a/src/backend/src/prisma/migrations/20241218031222_home_page_updates/migration.sql b/src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql similarity index 95% rename from src/backend/src/prisma/migrations/20241218031222_home_page_updates/migration.sql rename to src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql index c7975f2e21..f35a709d3f 100644 --- a/src/backend/src/prisma/migrations/20241218031222_home_page_updates/migration.sql +++ b/src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql @@ -8,8 +8,10 @@ ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT; CREATE TABLE "Announcement" ( "announcementId" TEXT NOT NULL, "text" TEXT NOT NULL, - "dateCrated" TIMESTAMP(3) NOT NULL, + "dateCreated" TIMESTAMP(3) NOT NULL, "userCreatedId" TEXT NOT NULL, + "slackEventId" TEXT NOT NULL, + "slackChannelName" TEXT NOT NULL, CONSTRAINT "Announcement_pkey" PRIMARY KEY ("announcementId") ); diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index e0157c3353..f70d812b2a 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -180,7 +180,7 @@ model User { deletedFrequentlyAskedQuestions FrequentlyAskedQuestion[] @relation(name: "frequentlyAskedQuestionDeleter") createdMilestones Milestone[] @relation(name: "milestoneCreator") deletedMilestones Milestone[] @relation(name: "milestoneDeleter") - receivedAnnouncements Announcement[] @relation(name: "receivedAnnouncements") + unreadAnnouncements Announcement[] @relation(name: "receivedAnnouncements") createdAnnouncements Announcement[] @relation(name: "createdAnnouncements") unreadNotifications Notification[] @relation(name: "userNotifications") } @@ -932,12 +932,14 @@ model Milestone { } model Announcement { - announcementId String @id @default(uuid()) - text String - usersReceived User[] @relation("receivedAnnouncements") - dateCrated DateTime - userCreatedId String - userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId]) + announcementId String @id @default(uuid()) + text String + usersReceived User[] @relation("receivedAnnouncements") + dateCreated DateTime + userCreatedId String + userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId]) + slackEventId String + slackChannelName String } model Notification { diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 622a6fb01c..802d586500 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -55,6 +55,7 @@ userRouter.post( UsersController.getManyUserTasks ); userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications); +userRouter.get('/:userId/announcements', UsersController.getUserUnreadAnnouncements); userRouter.post( '/:userId/notifications/remove', nonEmptyString(body('notificationId')), diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 1358ca5f6c..fff25ef56b 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -40,6 +40,8 @@ import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args'; import taskTransformer from '../transformers/tasks.transformer'; import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; import notificationTransformer from '../transformers/notifications.transformer'; +import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; +import announcementTransformer from '../transformers/announcements.transformer'; export default class UsersService { /** @@ -585,6 +587,22 @@ export default class UsersService { return requestedUser.unreadNotifications.map(notificationTransformer); } + /** + * Gets all of a user's unread announcements + * @param userId id of user to get unread announcements from + * @param organization the user's orgainzation + * @returns the unread announcements of the user + */ + static async getUserUnreadAnnouncements(userId: string, organization: Organization) { + const requestedUser = await prisma.user.findUnique({ + where: { userId }, + include: { unreadAnnouncements: getAnnouncementQueryArgs(organization.organizationId) } + }); + if (!requestedUser) throw new NotFoundException('User', userId); + + return requestedUser.unreadAnnouncements.map(announcementTransformer); + } + /** * Removes a notification from the user's unread notifications * @param userId id of the user to remove notification from diff --git a/src/backend/src/transformers/announcements.transformer.ts b/src/backend/src/transformers/announcements.transformer.ts new file mode 100644 index 0000000000..2a8f77e6b0 --- /dev/null +++ b/src/backend/src/transformers/announcements.transformer.ts @@ -0,0 +1,17 @@ +import { Prisma } from '@prisma/client'; +import { AnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; +import { Announcement } from 'shared'; +import { userTransformer } from './user.transformer'; + +const announcementTransformer = (announcement: Prisma.AnnouncementGetPayload): Announcement => { + return { + announcementId: announcement.announcementId, + text: announcement.text, + dateCreated: announcement.dateCreated, + userCreated: userTransformer(announcement.userCreated), + slackEventId: announcement.slackEventId, + slackChannelName: announcement.slackChannelName + }; +}; + +export default announcementTransformer; diff --git a/src/shared/index.ts b/src/shared/index.ts index 409dae2e65..40246d0fe4 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -12,6 +12,7 @@ export * from './src/types/task-types'; export * from './src/types/reimbursement-requests-types'; export * from './src/types/design-review-types'; export * from './src/types/notifications.types'; +export * from './src/types/announcements.types'; export * from './src/validate-wbs'; export * from './src/date-utils'; diff --git a/src/shared/src/types/announcements.types.ts b/src/shared/src/types/announcements.types.ts new file mode 100644 index 0000000000..9315535c86 --- /dev/null +++ b/src/shared/src/types/announcements.types.ts @@ -0,0 +1,10 @@ +import { User } from './user-types'; + +export interface Announcement { + announcementId: string; + text: string; + userCreated: User; + dateCreated: Date; + slackEventId: string; + slackChannelName: string; +} From cc6e64310744e8611fa00af32da8c49dcc4089ce Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Thu, 19 Dec 2024 07:58:15 -0500 Subject: [PATCH 02/17] #3074-updated announcements in schema --- .../src/prisma-query-args/announcements.query.args.ts | 3 +-- .../migration.sql | 9 +++++---- src/backend/src/prisma/schema.prisma | 11 +++++------ .../src/transformers/announcements.transformer.ts | 6 ++++-- src/shared/src/types/announcements.types.ts | 4 +++- 5 files changed, 18 insertions(+), 15 deletions(-) rename src/backend/src/prisma/migrations/{20241218044032_homepage_updates => 20241219125602_homepage_updates}/migration.sql (90%) diff --git a/src/backend/src/prisma-query-args/announcements.query.args.ts b/src/backend/src/prisma-query-args/announcements.query.args.ts index 2f0e1ba294..b88c9fbf1d 100644 --- a/src/backend/src/prisma-query-args/announcements.query.args.ts +++ b/src/backend/src/prisma-query-args/announcements.query.args.ts @@ -6,7 +6,6 @@ export type AnnouncementQueryArgs = ReturnType; export const getAnnouncementQueryArgs = (organizationId: string) => Prisma.validator()({ include: { - usersReceived: getUserQueryArgs(organizationId), - userCreated: getUserQueryArgs(organizationId) + usersReceived: getUserQueryArgs(organizationId) } }); diff --git a/src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql b/src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql similarity index 90% rename from src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql rename to src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql index f35a709d3f..37c468e58e 100644 --- a/src/backend/src/prisma/migrations/20241218044032_homepage_updates/migration.sql +++ b/src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql @@ -9,7 +9,8 @@ CREATE TABLE "Announcement" ( "announcementId" TEXT NOT NULL, "text" TEXT NOT NULL, "dateCreated" TIMESTAMP(3) NOT NULL, - "userCreatedId" TEXT NOT NULL, + "dateDeleted" TIMESTAMP(3), + "senderName" TEXT NOT NULL, "slackEventId" TEXT NOT NULL, "slackChannelName" TEXT NOT NULL, @@ -38,6 +39,9 @@ CREATE TABLE "_userNotifications" ( "B" TEXT NOT NULL ); +-- CreateIndex +CREATE UNIQUE INDEX "Announcement_slackEventId_key" ON "Announcement"("slackEventId"); + -- CreateIndex CREATE UNIQUE INDEX "_receivedAnnouncements_AB_unique" ON "_receivedAnnouncements"("A", "B"); @@ -53,9 +57,6 @@ CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B"); -- AddForeignKey ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE SET NULL ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "Announcement" ADD CONSTRAINT "Announcement_userCreatedId_fkey" FOREIGN KEY ("userCreatedId") REFERENCES "User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; - -- AddForeignKey ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index f70d812b2a..bb8247266d 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -181,7 +181,6 @@ model User { createdMilestones Milestone[] @relation(name: "milestoneCreator") deletedMilestones Milestone[] @relation(name: "milestoneDeleter") unreadAnnouncements Announcement[] @relation(name: "receivedAnnouncements") - createdAnnouncements Announcement[] @relation(name: "createdAnnouncements") unreadNotifications Notification[] @relation(name: "userNotifications") } @@ -932,13 +931,13 @@ model Milestone { } model Announcement { - announcementId String @id @default(uuid()) + announcementId String @id @default(uuid()) text String - usersReceived User[] @relation("receivedAnnouncements") + usersReceived User[] @relation("receivedAnnouncements") dateCreated DateTime - userCreatedId String - userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId]) - slackEventId String + dateDeleted DateTime? + senderName String + slackEventId String @unique slackChannelName String } diff --git a/src/backend/src/transformers/announcements.transformer.ts b/src/backend/src/transformers/announcements.transformer.ts index 2a8f77e6b0..4fee90eac2 100644 --- a/src/backend/src/transformers/announcements.transformer.ts +++ b/src/backend/src/transformers/announcements.transformer.ts @@ -7,10 +7,12 @@ const announcementTransformer = (announcement: Prisma.AnnouncementGetPayload Date: Thu, 19 Dec 2024 08:24:31 -0500 Subject: [PATCH 03/17] #3074-tests --- src/backend/src/prisma/seed.ts | 12 +++++- .../src/services/announcement.service.ts | 34 ++++++++++++++++ src/backend/tests/test-utils.ts | 1 + src/backend/tests/unmocked/users.test.ts | 39 ++++++++++++++++++- 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 src/backend/src/services/announcement.service.ts diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index fdb5ffefdb..ddd4e190cb 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -33,7 +33,7 @@ import { writeFileSync } from 'fs'; import WorkPackageTemplatesService from '../services/work-package-template.services'; import RecruitmentServices from '../services/recruitment.services'; import OrganizationsService from '../services/organizations.services'; -import NotificationsService from '../services/notifications.services'; +import AnnouncementService from '../services/announcement.service'; const prisma = new PrismaClient(); @@ -1894,6 +1894,16 @@ const performSeed: () => Promise = async () => { await RecruitmentServices.createFaq(batman, 'When was FinishLine created?', 'FinishLine was created in 2019', ner); await RecruitmentServices.createFaq(batman, 'How many developers are working on FinishLine?', '178 as of 2024', ner); + + await AnnouncementService.createAnnouncement( + 'Welcome to Finishline!', + [regina.userId], + new Date(), + 'Thomas Emrax', + '1', + 'software', + ner.organizationId + ); }; performSeed() diff --git a/src/backend/src/services/announcement.service.ts b/src/backend/src/services/announcement.service.ts new file mode 100644 index 0000000000..5b9fdab389 --- /dev/null +++ b/src/backend/src/services/announcement.service.ts @@ -0,0 +1,34 @@ +import { Announcement } from 'shared'; +import prisma from '../prisma/prisma'; +import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; +import announcementTransformer from '../transformers/announcements.transformer'; + +export default class AnnouncementService { + static async createAnnouncement( + text: string, + usersReceivedIds: string[], + dateCreated: Date, + senderName: string, + slackEventId: string, + slackChannelName: string, + organizationId: string + ): Promise { + const announcement = await prisma.announcement.create({ + data: { + text, + usersReceived: { + connect: usersReceivedIds.map((id) => ({ + userId: id + })) + }, + dateCreated, + senderName, + slackEventId, + slackChannelName + }, + ...getAnnouncementQueryArgs(organizationId) + }); + + return announcementTransformer(announcement); + } +} diff --git a/src/backend/tests/test-utils.ts b/src/backend/tests/test-utils.ts index 99f2a010e2..b399e44fe8 100644 --- a/src/backend/tests/test-utils.ts +++ b/src/backend/tests/test-utils.ts @@ -120,6 +120,7 @@ export const resetUsers = async () => { await prisma.frequentlyAskedQuestion.deleteMany(); await prisma.organization.deleteMany(); await prisma.user.deleteMany(); + await prisma.announcement.deleteMany(); }; export const createFinanceTeamAndLead = async (organization?: Organization) => { diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 512a651b90..789109a2ae 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -4,6 +4,7 @@ import { batmanAppAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; import NotificationsService from '../../src/services/notifications.services'; +import AnnouncementService from '../../src/services/announcement.service'; describe('User Tests', () => { let orgId: string; @@ -81,7 +82,7 @@ describe('User Tests', () => { ).rejects.toThrow(new NotFoundException('User', '1')); }); - it('Succeeds and gets user notifications', async () => { + it('Succeeds and removes user notification', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); @@ -102,4 +103,40 @@ describe('User Tests', () => { expect(updatedNotifications[0].text).toBe('test2'); }); }); + + describe('Get Announcements', () => { + it('fails on invalid user id', async () => { + await expect(async () => await UsersService.getUserUnreadAnnouncements('1', organization)).rejects.toThrow( + new NotFoundException('User', '1') + ); + }); + + it('Succeeds and gets user announcements', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await AnnouncementService.createAnnouncement( + 'test1', + [testBatman.userId], + new Date(), + 'Thomas Emrax', + '1', + 'software', + organization.organizationId + ); + await AnnouncementService.createAnnouncement( + 'test2', + [testBatman.userId], + new Date(), + 'Superman', + '50', + 'mechanical', + organization.organizationId + ); + + const announcements = await UsersService.getUserUnreadAnnouncements(testBatman.userId, organization); + + expect(announcements).toHaveLength(2); + expect(announcements[0].text).toBe('test1'); + expect(announcements[1].text).toBe('test2'); + }); + }); }); From 0dfedd2c315c5d2a6cc5e94078555d1c44d74f43 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Thu, 19 Dec 2024 19:20:51 -0500 Subject: [PATCH 04/17] #2074-javadocs --- src/backend/src/services/announcement.service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/backend/src/services/announcement.service.ts b/src/backend/src/services/announcement.service.ts index 5b9fdab389..eb0d9a5f1b 100644 --- a/src/backend/src/services/announcement.service.ts +++ b/src/backend/src/services/announcement.service.ts @@ -4,6 +4,18 @@ import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.que import announcementTransformer from '../transformers/announcements.transformer'; export default class AnnouncementService { + /** + * Creates an announcement that is sent to users + * this data is populated from slack events + * @param text slack message text + * @param usersReceivedIds users to send announcements to + * @param dateCreated date created of slack message + * @param senderName name of user who sent slack message + * @param slackEventId id of slack event (provided by slack api) + * @param slackChannelName name of channel message was sent in + * @param organizationId id of organization of users + * @returns the created announcement + */ static async createAnnouncement( text: string, usersReceivedIds: string[], From 66f5837a16e4a267cfbdc8232d1cbe862efebfab Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 20 Dec 2024 08:48:20 -0500 Subject: [PATCH 05/17] #3074-using current user --- .../src/controllers/users.controllers.ts | 21 +++++----- src/backend/src/routes/users.routes.ts | 10 ++--- src/backend/src/services/users.services.ts | 42 +++++++++++-------- src/backend/tests/unmocked/users.test.ts | 22 ---------- 4 files changed, 38 insertions(+), 57 deletions(-) diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index cc084c714e..b3fee931c2 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -194,10 +194,9 @@ export default class UsersController { static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { try { - const { userId } = req.params; - const { organization } = req; + const { organization, currentUser } = req; - const unreadNotifications = await UsersService.getUserUnreadNotifications(userId, organization); + const unreadNotifications = await UsersService.getUserUnreadNotifications(currentUser.userId, organization); res.status(200).json(unreadNotifications); } catch (error: unknown) { next(error); @@ -206,11 +205,14 @@ export default class UsersController { static async removeUserNotification(req: Request, res: Response, next: NextFunction) { try { - const { userId } = req.params; - const { notificationId } = req.body; - const { organization } = req; + const { notificationId } = req.params; + const { organization, currentUser } = req; - const unreadNotifications = await UsersService.removeUserNotification(userId, notificationId, organization); + const unreadNotifications = await UsersService.removeUserNotification( + currentUser.userId, + notificationId, + organization + ); res.status(200).json(unreadNotifications); } catch (error: unknown) { next(error); @@ -219,10 +221,9 @@ export default class UsersController { static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) { try { - const { userId } = req.params; - const { organization } = req; + const { organization, currentUser } = req; - const unreadAnnouncements = await UsersService.getUserUnreadAnnouncements(userId, organization); + const unreadAnnouncements = await UsersService.getUserUnreadAnnouncements(currentUser.userId, organization); res.status(200).json(unreadAnnouncements); } catch (error: unknown) { next(error); diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 802d586500..96be49828c 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,12 +54,8 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); -userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications); -userRouter.get('/:userId/announcements', UsersController.getUserUnreadAnnouncements); -userRouter.post( - '/:userId/notifications/remove', - nonEmptyString(body('notificationId')), - UsersController.removeUserNotification -); +userRouter.get('/notifications/current-user', UsersController.getUserUnreadNotifications); +userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements); +userRouter.post('/notifications/:notificationId/remove', UsersController.removeUserNotification); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index fff25ef56b..b537d019d2 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -578,45 +578,49 @@ export default class UsersService { * @returns the unread notifications of the user */ static async getUserUnreadNotifications(userId: string, organization: Organization) { - const requestedUser = await prisma.user.findUnique({ - where: { userId }, - include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } + const unreadNotifications = await prisma.notification.findMany({ + where: { + users: { + some: { userId } + } + }, + ...getNotificationQueryArgs(organization.organizationId) }); - if (!requestedUser) throw new NotFoundException('User', userId); - return requestedUser.unreadNotifications.map(notificationTransformer); + if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found'); + + return unreadNotifications.map(notificationTransformer); } /** * Gets all of a user's unread announcements - * @param userId id of user to get unread announcements from + * @param userId id of the current user * @param organization the user's orgainzation * @returns the unread announcements of the user */ static async getUserUnreadAnnouncements(userId: string, organization: Organization) { - const requestedUser = await prisma.user.findUnique({ - where: { userId }, - include: { unreadAnnouncements: getAnnouncementQueryArgs(organization.organizationId) } + const unreadAnnouncements = await prisma.announcement.findMany({ + where: { + usersReceived: { + some: { userId } + } + }, + ...getAnnouncementQueryArgs(organization.organizationId) }); - if (!requestedUser) throw new NotFoundException('User', userId); - return requestedUser.unreadAnnouncements.map(announcementTransformer); + if (!unreadAnnouncements) throw new HttpException(404, 'User Unread Announcements Not Found'); + + return unreadAnnouncements.map(announcementTransformer); } /** * Removes a notification from the user's unread notifications - * @param userId id of the user to remove notification from + * @param userId id of the current user * @param notificationId id of the notification to remove * @param organization the user's organization * @returns the user's updated unread notifications */ static async removeUserNotification(userId: string, notificationId: string, organization: Organization) { - const requestedUser = await prisma.user.findUnique({ - where: { userId } - }); - - if (!requestedUser) throw new NotFoundException('User', userId); - const updatedUser = await prisma.user.update({ where: { userId }, data: { @@ -629,6 +633,8 @@ export default class UsersService { include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } }); + if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`); + return updatedUser.unreadNotifications.map(notificationTransformer); } } diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 789109a2ae..bd0b934270 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -52,12 +52,6 @@ describe('User Tests', () => { }); describe('Get Notifications', () => { - it('fails on invalid user id', async () => { - await expect(async () => await UsersService.getUserUnreadNotifications('1', organization)).rejects.toThrow( - new NotFoundException('User', '1') - ); - }); - it('Succeeds and gets user notifications', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); @@ -72,16 +66,6 @@ describe('User Tests', () => { }); describe('Remove Notifications', () => { - it('Fails with invalid user', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); - const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); - - await expect( - async () => await UsersService.removeUserNotification('1', notifications[0].notificationId, organization) - ).rejects.toThrow(new NotFoundException('User', '1')); - }); - it('Succeeds and removes user notification', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); @@ -105,12 +89,6 @@ describe('User Tests', () => { }); describe('Get Announcements', () => { - it('fails on invalid user id', async () => { - await expect(async () => await UsersService.getUserUnreadAnnouncements('1', organization)).rejects.toThrow( - new NotFoundException('User', '1') - ); - }); - it('Succeeds and gets user announcements', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); await AnnouncementService.createAnnouncement( From 5a7cb3ddf7815e0dd0e77c89667a187ed0a54d19 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 20 Dec 2024 15:58:00 -0500 Subject: [PATCH 06/17] #3074-set date created to now in schema --- .../migration.sql | 2 +- src/backend/src/prisma/schema.prisma | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/backend/src/prisma/migrations/{20241219125602_homepage_updates => 20241220205608_homepage_redesign}/migration.sql (97%) diff --git a/src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql b/src/backend/src/prisma/migrations/20241220205608_homepage_redesign/migration.sql similarity index 97% rename from src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql rename to src/backend/src/prisma/migrations/20241220205608_homepage_redesign/migration.sql index 37c468e58e..9dc929526c 100644 --- a/src/backend/src/prisma/migrations/20241219125602_homepage_updates/migration.sql +++ b/src/backend/src/prisma/migrations/20241220205608_homepage_redesign/migration.sql @@ -8,7 +8,7 @@ ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT; CREATE TABLE "Announcement" ( "announcementId" TEXT NOT NULL, "text" TEXT NOT NULL, - "dateCreated" TIMESTAMP(3) NOT NULL, + "dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "dateDeleted" TIMESTAMP(3), "senderName" TEXT NOT NULL, "slackEventId" TEXT NOT NULL, diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index bb8247266d..1b7b2d5375 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -934,7 +934,7 @@ model Announcement { announcementId String @id @default(uuid()) text String usersReceived User[] @relation("receivedAnnouncements") - dateCreated DateTime + dateCreated DateTime @default(now()) dateDeleted DateTime? senderName String slackEventId String @unique From e71442b8294f0796974743a9d9fc0a34c38af415 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 20 Dec 2024 16:08:34 -0500 Subject: [PATCH 07/17] #3074-fixed service --- src/backend/src/services/announcement.service.ts | 2 -- src/backend/tests/unmocked/users.test.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/backend/src/services/announcement.service.ts b/src/backend/src/services/announcement.service.ts index eb0d9a5f1b..28f5c8e71b 100644 --- a/src/backend/src/services/announcement.service.ts +++ b/src/backend/src/services/announcement.service.ts @@ -19,7 +19,6 @@ export default class AnnouncementService { static async createAnnouncement( text: string, usersReceivedIds: string[], - dateCreated: Date, senderName: string, slackEventId: string, slackChannelName: string, @@ -33,7 +32,6 @@ export default class AnnouncementService { userId: id })) }, - dateCreated, senderName, slackEventId, slackChannelName diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index bd0b934270..a9f1488530 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -94,7 +94,6 @@ describe('User Tests', () => { await AnnouncementService.createAnnouncement( 'test1', [testBatman.userId], - new Date(), 'Thomas Emrax', '1', 'software', @@ -103,7 +102,6 @@ describe('User Tests', () => { await AnnouncementService.createAnnouncement( 'test2', [testBatman.userId], - new Date(), 'Superman', '50', 'mechanical', From 43b9bd0266b5c0a85207077d1f6478f3ec367e39 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 20 Dec 2024 16:12:08 -0500 Subject: [PATCH 08/17] #3074-fixed seed --- src/backend/src/prisma/seed.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index ddd4e190cb..e7b5aa4c84 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -1898,7 +1898,6 @@ const performSeed: () => Promise = async () => { await AnnouncementService.createAnnouncement( 'Welcome to Finishline!', [regina.userId], - new Date(), 'Thomas Emrax', '1', 'software', From 9ce32f92f4caca0f1e20a8d907842bd785c9bca2 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 20 Dec 2024 18:25:41 -0500 Subject: [PATCH 09/17] #3074-refactored notifications for route change --- src/backend/src/controllers/users.controllers.ts | 2 +- src/backend/src/routes/users.routes.ts | 2 +- src/frontend/src/apis/users.api.ts | 8 ++++---- src/frontend/src/components/NotificationAlert.tsx | 12 ++++-------- src/frontend/src/components/NotificationCard.tsx | 1 + src/frontend/src/hooks/users.hooks.ts | 14 +++++++------- src/frontend/src/pages/HomePage/Home.tsx | 2 +- src/frontend/src/utils/urls.ts | 4 ++-- 8 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index b3fee931c2..baa0d9228a 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -205,7 +205,7 @@ export default class UsersController { static async removeUserNotification(req: Request, res: Response, next: NextFunction) { try { - const { notificationId } = req.params; + const { notificationId } = req.body; const { organization, currentUser } = req; const unreadNotifications = await UsersService.removeUserNotification( diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 96be49828c..b859968e5a 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -56,6 +56,6 @@ userRouter.post( ); userRouter.get('/notifications/current-user', UsersController.getUserUnreadNotifications); userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements); -userRouter.post('/notifications/:notificationId/remove', UsersController.removeUserNotification); +userRouter.post('/notifications/remove', nonEmptyString(body('notificationId')), UsersController.removeUserNotification); export default userRouter; diff --git a/src/frontend/src/apis/users.api.ts b/src/frontend/src/apis/users.api.ts index 5a91bff5fd..4999c8cb66 100644 --- a/src/frontend/src/apis/users.api.ts +++ b/src/frontend/src/apis/users.api.ts @@ -164,8 +164,8 @@ export const getManyUserTasks = (userIds: string[]) => { /* * Gets all unread notifications of the user with the given id */ -export const getNotifications = (id: string) => { - return axios.get(apiUrls.userNotifications(id), { +export const getNotifications = () => { + return axios.get(apiUrls.userNotifications(), { transformResponse: (data) => JSON.parse(data) }); }; @@ -173,6 +173,6 @@ export const getNotifications = (id: string) => { /* * Removes a notification from the user with the given id */ -export const removeNotification = (userId: string, notificationId: string) => { - return axios.post(apiUrls.userRemoveNotifications(userId), { notificationId }); +export const removeNotification = (notificationId: string) => { + return axios.post(apiUrls.userRemoveNotifications(), { notificationId }); }; diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index 581d849ef0..668c0f3cfd 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -1,17 +1,13 @@ import { Box } from '@mui/material'; import React, { useEffect, useState } from 'react'; -import { Notification, User } from 'shared'; +import { Notification } from 'shared'; import NotificationCard from './NotificationCard'; import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks'; import { useHistory } from 'react-router-dom'; -interface NotificationAlertProps { - user: User; -} - -const NotificationAlert: React.FC = ({ user }) => { - const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications(user.userId); - const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(user.userId); +const NotificationAlert: React.FC = () => { + const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications(); + const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(); const [currentNotification, setCurrentNotification] = useState(); const history = useHistory(); diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index 1e4cfb4c02..38dd994029 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -37,6 +37,7 @@ const NotificationCard: React.FC = ({ notification, remov onClick={async () => await onClick(notification)} sx={{ display: 'flex', + alignItems: 'center', gap: 1, cursor: !!notification.eventLink ? 'pointer' : 'default' }} diff --git a/src/frontend/src/hooks/users.hooks.ts b/src/frontend/src/hooks/users.hooks.ts index 32279217f9..ffc7a65d75 100644 --- a/src/frontend/src/hooks/users.hooks.ts +++ b/src/frontend/src/hooks/users.hooks.ts @@ -269,9 +269,9 @@ export const useManyUserTasks = (userIds: string[]) => { * @param userId id of user to get unread notifications from * @returns */ -export const useUserNotifications = (userId: string) => { - return useQuery(['users', userId, 'notifications'], async () => { - const { data } = await getNotifications(userId); +export const useUserNotifications = () => { + return useQuery(['users', 'notifications'], async () => { + const { data } = await getNotifications(); return data; }); }; @@ -281,17 +281,17 @@ export const useUserNotifications = (userId: string) => { * @param userId id of user to get unread notifications from * @returns */ -export const useRemoveUserNotification = (userId: string) => { +export const useRemoveUserNotification = () => { const queryClient = useQueryClient(); return useMutation( - ['users', userId, 'notifications', 'remove'], + ['users', 'notifications', 'remove'], async (notification: Notification) => { - const { data } = await removeNotification(userId, notification.notificationId); + const { data } = await removeNotification(notification.notificationId); return data; }, { onSuccess: () => { - queryClient.invalidateQueries(['users', userId, 'notifications']); + queryClient.invalidateQueries(['users', 'notifications']); } } ); diff --git a/src/frontend/src/pages/HomePage/Home.tsx b/src/frontend/src/pages/HomePage/Home.tsx index 76db11f05a..f76ecc0e4c 100644 --- a/src/frontend/src/pages/HomePage/Home.tsx +++ b/src/frontend/src/pages/HomePage/Home.tsx @@ -18,7 +18,7 @@ const Home = () => { const [onMemberHomePage, setOnMemberHomePage] = useState(false); return ( <> - {!onMemberHomePage && } + {!onMemberHomePage && } {isGuest(user.role) && !onMemberHomePage ? ( ) : isMember(user.role) ? ( diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index d50675ed69..99c9d82c82 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -26,8 +26,8 @@ const userScheduleSettings = (id: string) => `${usersById(id)}/schedule-settings const userScheduleSettingsSet = () => `${users()}/schedule-settings/set`; const userTasks = (id: string) => `${usersById(id)}/tasks`; const manyUserTasks = () => `${users()}/tasks/get-many`; -const userNotifications = (id: string) => `${usersById(id)}/notifications`; -const userRemoveNotifications = (id: string) => `${usersById(id)}/notifications/remove`; +const userNotifications = () => `${users()}/notifications/current-user`; +const userRemoveNotifications = () => `${users()}/notifications/remove`; /**************** Projects Endpoints ****************/ const projects = () => `${API_URL}/projects`; From fd4f299f77347a6a9daf139d3f0b400f7f669b2e Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 20 Dec 2024 18:49:00 -0500 Subject: [PATCH 10/17] #3074-fixed transformer --- src/backend/src/transformers/announcements.transformer.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/backend/src/transformers/announcements.transformer.ts b/src/backend/src/transformers/announcements.transformer.ts index 4fee90eac2..8b43031390 100644 --- a/src/backend/src/transformers/announcements.transformer.ts +++ b/src/backend/src/transformers/announcements.transformer.ts @@ -5,13 +5,8 @@ import { userTransformer } from './user.transformer'; const announcementTransformer = (announcement: Prisma.AnnouncementGetPayload): Announcement => { return { - announcementId: announcement.announcementId, - text: announcement.text, + ...announcement, usersReceived: announcement.usersReceived.map(userTransformer), - dateCreated: announcement.dateCreated, - senderName: announcement.senderName, - slackEventId: announcement.slackEventId, - slackChannelName: announcement.slackChannelName, dateDeleted: announcement.dateDeleted ?? undefined }; }; From 0673cdf3168f24cdfdee189a5fb1378d9aadd79f Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sat, 21 Dec 2024 13:31:41 -0500 Subject: [PATCH 11/17] #3074-refactored notifications to separate router --- .../controllers/notifications.controllers.ts | 30 ++++++++++++ .../src/controllers/users.controllers.ts | 27 ----------- .../src/routes/notifications.routes.ts | 8 ++++ src/backend/src/routes/users.routes.ts | 2 - .../src/services/notifications.services.ts | 46 ++++++++++++++++++ src/backend/src/services/users.services.ts | 48 ------------------- src/backend/src/utils/auth.utils.ts | 2 +- src/backend/src/utils/notifications.utils.ts | 6 +-- .../tests/unmocked/notifications.test.ts | 43 +++++++++++++++++ src/backend/tests/unmocked/users.test.ts | 38 --------------- src/frontend/src/apis/notifications.api.ts | 19 ++++++++ src/frontend/src/apis/users.api.ts | 17 ------- .../src/components/NotificationAlert.tsx | 4 +- src/frontend/src/hooks/notifications.hooks.ts | 36 ++++++++++++++ src/frontend/src/hooks/users.hooks.ts | 40 +--------------- src/frontend/src/utils/urls.ts | 13 +++-- 16 files changed, 197 insertions(+), 182 deletions(-) create mode 100644 src/frontend/src/apis/notifications.api.ts create mode 100644 src/frontend/src/hooks/notifications.hooks.ts diff --git a/src/backend/src/controllers/notifications.controllers.ts b/src/backend/src/controllers/notifications.controllers.ts index bdf9c44f13..0d944632bd 100644 --- a/src/backend/src/controllers/notifications.controllers.ts +++ b/src/backend/src/controllers/notifications.controllers.ts @@ -11,4 +11,34 @@ export default class NotificationsController { next(error); } } + + static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { + try { + const { organization, currentUser } = req; + + const unreadNotifications = await NotificationsService.getUserUnreadNotifications( + currentUser.userId, + organization.organizationId + ); + res.status(200).json(unreadNotifications); + } catch (error: unknown) { + next(error); + } + } + + static async removeUserNotification(req: Request, res: Response, next: NextFunction) { + try { + const { notificationId } = req.body; + const { organization, currentUser } = req; + + const unreadNotifications = await NotificationsService.removeUserNotification( + currentUser.userId, + notificationId, + organization.organizationId + ); + res.status(200).json(unreadNotifications); + } catch (error: unknown) { + next(error); + } + } } diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index baa0d9228a..e9c0745b5c 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -192,33 +192,6 @@ export default class UsersController { } } - static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { - try { - const { organization, currentUser } = req; - - const unreadNotifications = await UsersService.getUserUnreadNotifications(currentUser.userId, organization); - res.status(200).json(unreadNotifications); - } catch (error: unknown) { - next(error); - } - } - - static async removeUserNotification(req: Request, res: Response, next: NextFunction) { - try { - const { notificationId } = req.body; - const { organization, currentUser } = req; - - const unreadNotifications = await UsersService.removeUserNotification( - currentUser.userId, - notificationId, - organization - ); - res.status(200).json(unreadNotifications); - } catch (error: unknown) { - next(error); - } - } - static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) { try { const { organization, currentUser } = req; diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index 4701b0f3ea..95cde92c09 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -1,8 +1,16 @@ import express from 'express'; import NotificationsController from '../controllers/notifications.controllers'; +import { nonEmptyString } from '../utils/validation.utils'; +import { body } from 'express-validator'; const notificationsRouter = express.Router(); notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications); +notificationsRouter.get('/current-user', NotificationsController.getUserUnreadNotifications); +notificationsRouter.post( + '/curent-user/remove', + nonEmptyString(body('notificationId')), + NotificationsController.removeUserNotification +); export default notificationsRouter; diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index b859968e5a..077e2fd02f 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,8 +54,6 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); -userRouter.get('/notifications/current-user', UsersController.getUserUnreadNotifications); userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements); -userRouter.post('/notifications/remove', nonEmptyString(body('notificationId')), UsersController.removeUserNotification); export default userRouter; diff --git a/src/backend/src/services/notifications.services.ts b/src/backend/src/services/notifications.services.ts index e0617301f5..3da62b744f 100644 --- a/src/backend/src/services/notifications.services.ts +++ b/src/backend/src/services/notifications.services.ts @@ -196,6 +196,52 @@ export default class NotificationsService { await Promise.all(promises); } + /** + * Gets all of a user's unread notifications + * @param userId id of user to get unread notifications from + * @param organization the user's orgainzation + * @returns the unread notifications of the user + */ + static async getUserUnreadNotifications(userId: string, organizationId: string) { + const unreadNotifications = await prisma.notification.findMany({ + where: { + users: { + some: { userId } + } + }, + ...getNotificationQueryArgs(organizationId) + }); + + if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found'); + + return unreadNotifications.map(notificationTransformer); + } + + /** + * Removes a notification from the user's unread notifications + * @param userId id of the current user + * @param notificationId id of the notification to remove + * @param organization the user's organization + * @returns the user's updated unread notifications + */ + static async removeUserNotification(userId: string, notificationId: string, organizationId: string) { + const updatedUser = await prisma.user.update({ + where: { userId }, + data: { + unreadNotifications: { + disconnect: { + notificationId + } + } + }, + include: { unreadNotifications: getNotificationQueryArgs(organizationId) } + }); + + if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`); + + return updatedUser.unreadNotifications.map(notificationTransformer); + } + /** * Creates and sends a notification to all users with the given userIds * @param text writing in the notification diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index b537d019d2..3dc8766113 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -38,8 +38,6 @@ import { getAuthUserQueryArgs } from '../prisma-query-args/auth-user.query-args' import authenticatedUserTransformer from '../transformers/auth-user.transformer'; import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args'; import taskTransformer from '../transformers/tasks.transformer'; -import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; -import notificationTransformer from '../transformers/notifications.transformer'; import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; import announcementTransformer from '../transformers/announcements.transformer'; @@ -571,27 +569,6 @@ export default class UsersService { return resolvedTasks.flat(); } - /** - * Gets all of a user's unread notifications - * @param userId id of user to get unread notifications from - * @param organization the user's orgainzation - * @returns the unread notifications of the user - */ - static async getUserUnreadNotifications(userId: string, organization: Organization) { - const unreadNotifications = await prisma.notification.findMany({ - where: { - users: { - some: { userId } - } - }, - ...getNotificationQueryArgs(organization.organizationId) - }); - - if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found'); - - return unreadNotifications.map(notificationTransformer); - } - /** * Gets all of a user's unread announcements * @param userId id of the current user @@ -612,29 +589,4 @@ export default class UsersService { return unreadAnnouncements.map(announcementTransformer); } - - /** - * Removes a notification from the user's unread notifications - * @param userId id of the current user - * @param notificationId id of the notification to remove - * @param organization the user's organization - * @returns the user's updated unread notifications - */ - static async removeUserNotification(userId: string, notificationId: string, organization: Organization) { - const updatedUser = await prisma.user.update({ - where: { userId }, - data: { - unreadNotifications: { - disconnect: { - notificationId - } - } - }, - include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } - }); - - if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`); - - return updatedUser.unreadNotifications.map(notificationTransformer); - } } diff --git a/src/backend/src/utils/auth.utils.ts b/src/backend/src/utils/auth.utils.ts index 8520d881e4..7e7602eb6a 100644 --- a/src/backend/src/utils/auth.utils.ts +++ b/src/backend/src/utils/auth.utils.ts @@ -66,7 +66,7 @@ export const requireJwtDev = (req: Request, res: Response, next: NextFunction) = ) { next(); } else if ( - req.path.startsWith('/notifications') // task deadline notification endpoint + req.path.startsWith('/notifications/taskdeadlines') // task deadline notification endpoint ) { notificationEndpointAuth(req, res, next); } else { diff --git a/src/backend/src/utils/notifications.utils.ts b/src/backend/src/utils/notifications.utils.ts index c47ba9b7a0..445f36ed67 100644 --- a/src/backend/src/utils/notifications.utils.ts +++ b/src/backend/src/utils/notifications.utils.ts @@ -78,11 +78,7 @@ export const sendHomeCrReviewedNotification = async ( accepted: boolean, organizationId: string ) => { - const isProd = process.env.NODE_ENV === 'production'; - - const changeRequestLink = isProd - ? `https://finishlinebyner.com/change-requests/${changeRequest.crId}` - : `http://localhost:3000/change-requests/${changeRequest.crId}`; + const changeRequestLink = `/change-requests/${changeRequest.crId}`; await NotificationsService.sendNotifcationToUsers( `CR #${changeRequest.identifier} has been ${accepted ? 'approved!' : 'denied.'}`, accepted ? 'check_circle' : 'cancel', diff --git a/src/backend/tests/unmocked/notifications.test.ts b/src/backend/tests/unmocked/notifications.test.ts index d3cce68361..026099c1c3 100644 --- a/src/backend/tests/unmocked/notifications.test.ts +++ b/src/backend/tests/unmocked/notifications.test.ts @@ -56,4 +56,47 @@ describe('Notifications Tests', () => { expect(supermanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); }); }); + + describe('Get Notifications', () => { + it('Succeeds and gets user notifications', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); + + const notifications = await NotificationService.getUserUnreadNotifications( + testBatman.userId, + organization.organizationId + ); + + expect(notifications).toHaveLength(2); + expect(notifications[0].text).toBe('test1'); + expect(notifications[1].text).toBe('test2'); + }); + }); + + describe('Remove Notifications', () => { + it('Succeeds and removes user notification', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); + await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); + + const notifications = await NotificationService.getUserUnreadNotifications( + testBatman.userId, + organization.organizationId + ); + + expect(notifications).toHaveLength(2); + expect(notifications[0].text).toBe('test1'); + expect(notifications[1].text).toBe('test2'); + + const updatedNotifications = await NotificationService.removeUserNotification( + testBatman.userId, + notifications[0].notificationId, + organization.organizationId + ); + + expect(updatedNotifications).toHaveLength(1); + expect(updatedNotifications[0].text).toBe('test2'); + }); + }); }); diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index a9f1488530..c78a42d76f 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -3,7 +3,6 @@ import { createTestOrganization, createTestTask, createTestUser, resetUsers } fr import { batmanAppAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; -import NotificationsService from '../../src/services/notifications.services'; import AnnouncementService from '../../src/services/announcement.service'; describe('User Tests', () => { @@ -51,43 +50,6 @@ describe('User Tests', () => { }); }); - describe('Get Notifications', () => { - it('Succeeds and gets user notifications', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); - await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); - - const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); - - expect(notifications).toHaveLength(2); - expect(notifications[0].text).toBe('test1'); - expect(notifications[1].text).toBe('test2'); - }); - }); - - describe('Remove Notifications', () => { - it('Succeeds and removes user notification', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); - await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); - - const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); - - expect(notifications).toHaveLength(2); - expect(notifications[0].text).toBe('test1'); - expect(notifications[1].text).toBe('test2'); - - const updatedNotifications = await UsersService.removeUserNotification( - testBatman.userId, - notifications[0].notificationId, - organization - ); - - expect(updatedNotifications).toHaveLength(1); - expect(updatedNotifications[0].text).toBe('test2'); - }); - }); - describe('Get Announcements', () => { it('Succeeds and gets user announcements', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); diff --git a/src/frontend/src/apis/notifications.api.ts b/src/frontend/src/apis/notifications.api.ts new file mode 100644 index 0000000000..a7cbd1894c --- /dev/null +++ b/src/frontend/src/apis/notifications.api.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; +import { apiUrls } from '../utils/urls'; +import { Notification } from 'shared'; + +/* + * Gets all unread notifications of the user with the given id + */ +export const getNotifications = () => { + return axios.get(apiUrls.notificationsCurrentUser(), { + transformResponse: (data) => JSON.parse(data) + }); +}; + +/* + * Removes a notification from the user with the given id + */ +export const removeNotification = (notificationId: string) => { + return axios.post(apiUrls.notificationsRemoveCurrentUser(), { notificationId }); +}; diff --git a/src/frontend/src/apis/users.api.ts b/src/frontend/src/apis/users.api.ts index 4999c8cb66..afa5ea00f6 100644 --- a/src/frontend/src/apis/users.api.ts +++ b/src/frontend/src/apis/users.api.ts @@ -5,7 +5,6 @@ import axios from '../utils/axios'; import { - Notification, Project, SetUserScheduleSettingsPayload, Task, @@ -160,19 +159,3 @@ export const getManyUserTasks = (userIds: string[]) => { } ); }; - -/* - * Gets all unread notifications of the user with the given id - */ -export const getNotifications = () => { - return axios.get(apiUrls.userNotifications(), { - transformResponse: (data) => JSON.parse(data) - }); -}; - -/* - * Removes a notification from the user with the given id - */ -export const removeNotification = (notificationId: string) => { - return axios.post(apiUrls.userRemoveNotifications(), { notificationId }); -}; diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx index 668c0f3cfd..53b01a5aaa 100644 --- a/src/frontend/src/components/NotificationAlert.tsx +++ b/src/frontend/src/components/NotificationAlert.tsx @@ -2,11 +2,11 @@ import { Box } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { Notification } from 'shared'; import NotificationCard from './NotificationCard'; -import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks'; import { useHistory } from 'react-router-dom'; +import { useCurrentUserNotifications, useRemoveUserNotification } from '../hooks/notifications.hooks'; const NotificationAlert: React.FC = () => { - const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications(); + const { data: notifications, isLoading: notificationsIsLoading } = useCurrentUserNotifications(); const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(); const [currentNotification, setCurrentNotification] = useState(); const history = useHistory(); diff --git a/src/frontend/src/hooks/notifications.hooks.ts b/src/frontend/src/hooks/notifications.hooks.ts new file mode 100644 index 0000000000..0f7daea18b --- /dev/null +++ b/src/frontend/src/hooks/notifications.hooks.ts @@ -0,0 +1,36 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { Notification } from 'shared'; +import { getNotifications, removeNotification } from '../apis/notifications.api'; + +/** + * Curstom react hook to get all unread notifications from a user + * @param userId id of user to get unread notifications from + * @returns + */ +export const useCurrentUserNotifications = () => { + return useQuery(['notifications', 'current-user'], async () => { + const { data } = await getNotifications(); + return data; + }); +}; + +/** + * Curstom react hook to remove a notification from a user's unread notifications + * @param userId id of user to get unread notifications from + * @returns + */ +export const useRemoveUserNotification = () => { + const queryClient = useQueryClient(); + return useMutation( + ['notifications', 'currentUser', 'remove'], + async (notification: Notification) => { + const { data } = await removeNotification(notification.notificationId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['notifications', 'currentUser']); + } + } + ); +}; diff --git a/src/frontend/src/hooks/users.hooks.ts b/src/frontend/src/hooks/users.hooks.ts index ffc7a65d75..96b659c1f1 100644 --- a/src/frontend/src/hooks/users.hooks.ts +++ b/src/frontend/src/hooks/users.hooks.ts @@ -19,9 +19,7 @@ import { getUserScheduleSettings, updateUserScheduleSettings, getUserTasks, - getManyUserTasks, - getNotifications, - removeNotification + getManyUserTasks } from '../apis/users.api'; import { User, @@ -33,8 +31,7 @@ import { UserScheduleSettings, UserWithScheduleSettings, SetUserScheduleSettingsPayload, - Task, - Notification + Task } from 'shared'; import { useAuth } from './auth.hooks'; import { useContext } from 'react'; @@ -263,36 +260,3 @@ export const useManyUserTasks = (userIds: string[]) => { return data; }); }; - -/** - * Curstom react hook to get all unread notifications from a user - * @param userId id of user to get unread notifications from - * @returns - */ -export const useUserNotifications = () => { - return useQuery(['users', 'notifications'], async () => { - const { data } = await getNotifications(); - return data; - }); -}; - -/** - * Curstom react hook to remove a notification from a user's unread notifications - * @param userId id of user to get unread notifications from - * @returns - */ -export const useRemoveUserNotification = () => { - const queryClient = useQueryClient(); - return useMutation( - ['users', 'notifications', 'remove'], - async (notification: Notification) => { - const { data } = await removeNotification(notification.notificationId); - return data; - }, - { - onSuccess: () => { - queryClient.invalidateQueries(['users', 'notifications']); - } - } - ); -}; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 99c9d82c82..93d6d80cf0 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -26,8 +26,6 @@ const userScheduleSettings = (id: string) => `${usersById(id)}/schedule-settings const userScheduleSettingsSet = () => `${users()}/schedule-settings/set`; const userTasks = (id: string) => `${usersById(id)}/tasks`; const manyUserTasks = () => `${users()}/tasks/get-many`; -const userNotifications = () => `${users()}/notifications/current-user`; -const userRemoveNotifications = () => `${users()}/notifications/remove`; /**************** Projects Endpoints ****************/ const projects = () => `${API_URL}/projects`; @@ -196,6 +194,11 @@ const faqCreate = () => `${recruitment()}/faq/create`; const faqEdit = (id: string) => `${recruitment()}/faq/${id}/edit`; const faqDelete = (id: string) => `${recruitment()}/faq/${id}/delete`; +/************** Notification Endpoints ***************/ +const notifications = () => `${API_URL}/notifications`; +const notificationsCurrentUser = () => `${notifications()}/current-user`; +const notificationsRemoveCurrentUser = () => `${notificationsCurrentUser()}/remove`; + /**************** Other Endpoints ****************/ const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`; @@ -214,8 +217,6 @@ export const apiUrls = { userScheduleSettingsSet, userTasks, manyUserTasks, - userNotifications, - userRemoveNotifications, projects, allProjects, @@ -355,5 +356,9 @@ export const apiUrls = { faqEdit, faqDelete, + notifications, + notificationsCurrentUser, + notificationsRemoveCurrentUser, + version }; From 962e88c52bdae3c4e6037ab80ad7c1af81cfdf61 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sat, 21 Dec 2024 13:45:50 -0500 Subject: [PATCH 12/17] #3074-refactored announcements to be on separate route --- src/backend/index.ts | 2 + .../controllers/announcements.controllers.ts | 18 +++++++ .../src/controllers/users.controllers.ts | 11 ----- .../src/routes/announcements.routes.ts | 6 +++ src/backend/src/routes/users.routes.ts | 1 - .../src/services/announcement.service.ts | 22 +++++++++ src/backend/src/services/users.services.ts | 23 --------- .../tests/unmocked/announcements.test.ts | 47 +++++++++++++++++++ src/backend/tests/unmocked/users.test.ts | 29 ------------ 9 files changed, 95 insertions(+), 64 deletions(-) create mode 100644 src/backend/src/controllers/announcements.controllers.ts create mode 100644 src/backend/src/routes/announcements.routes.ts create mode 100644 src/backend/tests/unmocked/announcements.test.ts diff --git a/src/backend/index.ts b/src/backend/index.ts index babf50b843..8f4289a81d 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -17,6 +17,7 @@ import workPackageTemplatesRouter from './src/routes/work-package-templates.rout import carsRouter from './src/routes/cars.routes'; import organizationRouter from './src/routes/organizations.routes'; import recruitmentRouter from './src/routes/recruitment.routes'; +import announcementsRouter from './src/routes/announcements.routes'; const app = express(); @@ -68,6 +69,7 @@ app.use('/templates', workPackageTemplatesRouter); app.use('/cars', carsRouter); app.use('/organizations', organizationRouter); app.use('/recruitment', recruitmentRouter); +app.use('/announcements', announcementsRouter); app.use('/', (_req, res) => { res.status(200).json('Welcome to FinishLine'); }); diff --git a/src/backend/src/controllers/announcements.controllers.ts b/src/backend/src/controllers/announcements.controllers.ts new file mode 100644 index 0000000000..e5ccafbe06 --- /dev/null +++ b/src/backend/src/controllers/announcements.controllers.ts @@ -0,0 +1,18 @@ +import { NextFunction, Request, Response } from 'express'; +import AnnouncementService from '../services/announcement.service'; + +export default class AnnouncementController { + static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) { + try { + const { organization, currentUser } = req; + + const unreadAnnouncements = await AnnouncementService.getUserUnreadAnnouncements( + currentUser.userId, + organization.organizationId + ); + res.status(200).json(unreadAnnouncements); + } catch (error: unknown) { + next(error); + } + } +} diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index e9c0745b5c..8076e225d7 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -191,15 +191,4 @@ export default class UsersController { next(error); } } - - static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) { - try { - const { organization, currentUser } = req; - - const unreadAnnouncements = await UsersService.getUserUnreadAnnouncements(currentUser.userId, organization); - res.status(200).json(unreadAnnouncements); - } catch (error: unknown) { - next(error); - } - } } diff --git a/src/backend/src/routes/announcements.routes.ts b/src/backend/src/routes/announcements.routes.ts new file mode 100644 index 0000000000..dbdb1fa295 --- /dev/null +++ b/src/backend/src/routes/announcements.routes.ts @@ -0,0 +1,6 @@ +import express from 'express'; +import AnnouncementController from '../controllers/announcements.controllers'; + +const announcementsRouter = express.Router(); + +announcementsRouter.get('/announcements/current-user', AnnouncementController.getUserUnreadAnnouncements); diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 077e2fd02f..2f95201f6f 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,6 +54,5 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); -userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements); export default userRouter; diff --git a/src/backend/src/services/announcement.service.ts b/src/backend/src/services/announcement.service.ts index 28f5c8e71b..7562755f01 100644 --- a/src/backend/src/services/announcement.service.ts +++ b/src/backend/src/services/announcement.service.ts @@ -2,6 +2,7 @@ import { Announcement } from 'shared'; import prisma from '../prisma/prisma'; import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; import announcementTransformer from '../transformers/announcements.transformer'; +import { HttpException } from '../utils/errors.utils'; export default class AnnouncementService { /** @@ -41,4 +42,25 @@ export default class AnnouncementService { return announcementTransformer(announcement); } + + /** + * Gets all of a user's unread announcements + * @param userId id of the current user + * @param organization the user's orgainzation + * @returns the unread announcements of the user + */ + static async getUserUnreadAnnouncements(userId: string, organizationId: string) { + const unreadAnnouncements = await prisma.announcement.findMany({ + where: { + usersReceived: { + some: { userId } + } + }, + ...getAnnouncementQueryArgs(organizationId) + }); + + if (!unreadAnnouncements) throw new HttpException(404, 'User Unread Announcements Not Found'); + + return unreadAnnouncements.map(announcementTransformer); + } } diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 3dc8766113..d786c04137 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -38,8 +38,6 @@ import { getAuthUserQueryArgs } from '../prisma-query-args/auth-user.query-args' import authenticatedUserTransformer from '../transformers/auth-user.transformer'; import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args'; import taskTransformer from '../transformers/tasks.transformer'; -import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args'; -import announcementTransformer from '../transformers/announcements.transformer'; export default class UsersService { /** @@ -568,25 +566,4 @@ export default class UsersService { const resolvedTasks = await Promise.all(tasksPromises); return resolvedTasks.flat(); } - - /** - * Gets all of a user's unread announcements - * @param userId id of the current user - * @param organization the user's orgainzation - * @returns the unread announcements of the user - */ - static async getUserUnreadAnnouncements(userId: string, organization: Organization) { - const unreadAnnouncements = await prisma.announcement.findMany({ - where: { - usersReceived: { - some: { userId } - } - }, - ...getAnnouncementQueryArgs(organization.organizationId) - }); - - if (!unreadAnnouncements) throw new HttpException(404, 'User Unread Announcements Not Found'); - - return unreadAnnouncements.map(announcementTransformer); - } } diff --git a/src/backend/tests/unmocked/announcements.test.ts b/src/backend/tests/unmocked/announcements.test.ts new file mode 100644 index 0000000000..534b546edb --- /dev/null +++ b/src/backend/tests/unmocked/announcements.test.ts @@ -0,0 +1,47 @@ +import { Organization } from '@prisma/client'; +import { batmanAppAdmin } from '../test-data/users.test-data'; +import { createTestOrganization, createTestUser, resetUsers } from '../test-utils'; +import AnnouncementService from '../../src/services/announcement.service'; + +describe('Announcemnts Tests', () => { + let orgId: string; + let organization: Organization; + beforeEach(async () => { + organization = await createTestOrganization(); + orgId = organization.organizationId; + }); + + afterEach(async () => { + await resetUsers(); + }); + describe('Get Announcements', () => { + it('Succeeds and gets user announcements', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await AnnouncementService.createAnnouncement( + 'test1', + [testBatman.userId], + 'Thomas Emrax', + '1', + 'software', + organization.organizationId + ); + await AnnouncementService.createAnnouncement( + 'test2', + [testBatman.userId], + 'Superman', + '50', + 'mechanical', + organization.organizationId + ); + + const announcements = await AnnouncementService.getUserUnreadAnnouncements( + testBatman.userId, + organization.organizationId + ); + + expect(announcements).toHaveLength(2); + expect(announcements[0].text).toBe('test1'); + expect(announcements[1].text).toBe('test2'); + }); + }); +}); diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index c78a42d76f..c13a0c857f 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -3,7 +3,6 @@ import { createTestOrganization, createTestTask, createTestUser, resetUsers } fr import { batmanAppAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; -import AnnouncementService from '../../src/services/announcement.service'; describe('User Tests', () => { let orgId: string; @@ -49,32 +48,4 @@ describe('User Tests', () => { expect(userTasks).toStrictEqual([batmanTask, batmanTask]); }); }); - - describe('Get Announcements', () => { - it('Succeeds and gets user announcements', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await AnnouncementService.createAnnouncement( - 'test1', - [testBatman.userId], - 'Thomas Emrax', - '1', - 'software', - organization.organizationId - ); - await AnnouncementService.createAnnouncement( - 'test2', - [testBatman.userId], - 'Superman', - '50', - 'mechanical', - organization.organizationId - ); - - const announcements = await UsersService.getUserUnreadAnnouncements(testBatman.userId, organization); - - expect(announcements).toHaveLength(2); - expect(announcements[0].text).toBe('test1'); - expect(announcements[1].text).toBe('test2'); - }); - }); }); From d95527d6b26ee5854fb530da54b26398aae5c319 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sat, 21 Dec 2024 17:58:18 -0500 Subject: [PATCH 13/17] #3074-fixed issues --- src/backend/src/routes/announcements.routes.ts | 2 ++ src/backend/src/routes/notifications.routes.ts | 2 +- src/frontend/src/apis/notifications.api.ts | 2 +- src/frontend/src/hooks/notifications.hooks.ts | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/backend/src/routes/announcements.routes.ts b/src/backend/src/routes/announcements.routes.ts index dbdb1fa295..4fa7696e72 100644 --- a/src/backend/src/routes/announcements.routes.ts +++ b/src/backend/src/routes/announcements.routes.ts @@ -4,3 +4,5 @@ import AnnouncementController from '../controllers/announcements.controllers'; const announcementsRouter = express.Router(); announcementsRouter.get('/announcements/current-user', AnnouncementController.getUserUnreadAnnouncements); + +export default announcementsRouter; diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index 95cde92c09..0aa23f3feb 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -8,7 +8,7 @@ const notificationsRouter = express.Router(); notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications); notificationsRouter.get('/current-user', NotificationsController.getUserUnreadNotifications); notificationsRouter.post( - '/curent-user/remove', + '/current-user/remove', nonEmptyString(body('notificationId')), NotificationsController.removeUserNotification ); diff --git a/src/frontend/src/apis/notifications.api.ts b/src/frontend/src/apis/notifications.api.ts index a7cbd1894c..6a702964d9 100644 --- a/src/frontend/src/apis/notifications.api.ts +++ b/src/frontend/src/apis/notifications.api.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios from '../utils/axios'; import { apiUrls } from '../utils/urls'; import { Notification } from 'shared'; diff --git a/src/frontend/src/hooks/notifications.hooks.ts b/src/frontend/src/hooks/notifications.hooks.ts index 0f7daea18b..5ca9ae621b 100644 --- a/src/frontend/src/hooks/notifications.hooks.ts +++ b/src/frontend/src/hooks/notifications.hooks.ts @@ -22,14 +22,14 @@ export const useCurrentUserNotifications = () => { export const useRemoveUserNotification = () => { const queryClient = useQueryClient(); return useMutation( - ['notifications', 'currentUser', 'remove'], + ['notifications', 'current-user', 'remove'], async (notification: Notification) => { const { data } = await removeNotification(notification.notificationId); return data; }, { onSuccess: () => { - queryClient.invalidateQueries(['notifications', 'currentUser']); + queryClient.invalidateQueries(['notifications', 'current-user']); } } ); From b3132f043504df888c2f7ccb7309671339629bad Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sat, 21 Dec 2024 18:33:49 -0500 Subject: [PATCH 14/17] #3074-small fix --- src/backend/src/routes/announcements.routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/src/routes/announcements.routes.ts b/src/backend/src/routes/announcements.routes.ts index 4fa7696e72..b772f09fbb 100644 --- a/src/backend/src/routes/announcements.routes.ts +++ b/src/backend/src/routes/announcements.routes.ts @@ -3,6 +3,6 @@ import AnnouncementController from '../controllers/announcements.controllers'; const announcementsRouter = express.Router(); -announcementsRouter.get('/announcements/current-user', AnnouncementController.getUserUnreadAnnouncements); +announcementsRouter.get('/current-user', AnnouncementController.getUserUnreadAnnouncements); export default announcementsRouter; From 2be6d82649af19a38c670ab90af62b741359dd65 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 22 Dec 2024 07:48:00 -0500 Subject: [PATCH 15/17] #3074-linting --- src/backend/src/routes/notifications.routes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index 624dbc183c..d5411bddde 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -1,7 +1,5 @@ import express from 'express'; import NotificationsController from '../controllers/notifications.controllers'; -import { nonEmptyString } from '../utils/validation.utils'; -import { body } from 'express-validator'; const notificationsRouter = express.Router(); From 329b60fff17de2f8be779868259b50f0ca8364ec Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 22 Dec 2024 10:57:40 -0500 Subject: [PATCH 16/17] #3074-refactored notifications into pop-ups --- src/backend/index.ts | 2 + .../controllers/notifications.controllers.ts | 30 ------ .../src/controllers/popUps.controllers.ts | 27 +++++ .../notifications.query-args.ts | 11 -- .../prisma-query-args/pop-up.query-args.ts | 11 ++ .../migration.sql | 18 ++-- src/backend/src/prisma/schema.prisma | 16 +-- src/backend/src/prisma/seed.ts | 1 + .../src/routes/notifications.routes.ts | 2 - src/backend/src/routes/pop-up.routes.ts | 9 ++ .../src/services/announcement.service.ts | 2 + .../src/services/change-requests.services.ts | 6 +- .../src/services/design-reviews.services.ts | 4 +- .../src/services/notifications.services.ts | 98 +---------------- src/backend/src/services/pop-up.services.ts | 100 +++++++++++++++++ .../transformers/notifications.transformer.ts | 14 --- .../src/transformers/pop-up.transformer.ts | 12 +++ src/backend/src/utils/auth.utils.ts | 2 +- src/backend/src/utils/notifications.utils.ts | 75 +------------ src/backend/src/utils/pop-up.utils.ts | 69 ++++++++++++ .../tests/unmocked/announcements.test.ts | 2 + .../tests/unmocked/notifications.test.ts | 102 ------------------ src/backend/tests/unmocked/pop-up.test.ts | 90 ++++++++++++++++ src/frontend/src/apis/notifications.api.ts | 19 ---- src/frontend/src/apis/pop-ups.api.ts | 19 ++++ .../src/components/NotificationAlert.tsx | 53 --------- .../src/components/NotificationCard.tsx | 24 ++--- src/frontend/src/components/PopUpAlert.tsx | 49 +++++++++ src/frontend/src/hooks/notifications.hooks.ts | 36 ------- src/frontend/src/hooks/pop-ups.hooks.ts | 36 +++++++ src/frontend/src/pages/HomePage/Home.tsx | 4 +- src/frontend/src/utils/urls.ts | 14 +-- src/shared/index.ts | 2 +- src/shared/src/types/announcements.types.ts | 2 +- ...notifications.types.ts => pop-up-types.ts} | 4 +- 35 files changed, 479 insertions(+), 486 deletions(-) create mode 100644 src/backend/src/controllers/popUps.controllers.ts delete mode 100644 src/backend/src/prisma-query-args/notifications.query-args.ts create mode 100644 src/backend/src/prisma-query-args/pop-up.query-args.ts rename src/backend/src/prisma/migrations/{20241222123937_homepage_redesign => 20241222150147_homepage_redesign}/migration.sql (70%) create mode 100644 src/backend/src/routes/pop-up.routes.ts create mode 100644 src/backend/src/services/pop-up.services.ts delete mode 100644 src/backend/src/transformers/notifications.transformer.ts create mode 100644 src/backend/src/transformers/pop-up.transformer.ts create mode 100644 src/backend/src/utils/pop-up.utils.ts delete mode 100644 src/backend/tests/unmocked/notifications.test.ts create mode 100644 src/backend/tests/unmocked/pop-up.test.ts delete mode 100644 src/frontend/src/apis/notifications.api.ts create mode 100644 src/frontend/src/apis/pop-ups.api.ts delete mode 100644 src/frontend/src/components/NotificationAlert.tsx create mode 100644 src/frontend/src/components/PopUpAlert.tsx delete mode 100644 src/frontend/src/hooks/notifications.hooks.ts create mode 100644 src/frontend/src/hooks/pop-ups.hooks.ts rename src/shared/src/types/{notifications.types.ts => pop-up-types.ts} (50%) diff --git a/src/backend/index.ts b/src/backend/index.ts index 8a505bff76..86470eb0a0 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -19,6 +19,7 @@ import organizationRouter from './src/routes/organizations.routes'; import recruitmentRouter from './src/routes/recruitment.routes'; import announcementsRouter from './src/routes/announcements.routes'; import onboardingRouter from './src/routes/onboarding.routes'; +import popUpsRouter from './src/routes/pop-up.routes'; const app = express(); @@ -70,6 +71,7 @@ app.use('/templates', workPackageTemplatesRouter); app.use('/cars', carsRouter); app.use('/organizations', organizationRouter); app.use('/recruitment', recruitmentRouter); +app.use('/pop-ups', popUpsRouter); app.use('/announcements', announcementsRouter); app.use('/onboarding', onboardingRouter); app.use('/', (_req, res) => { diff --git a/src/backend/src/controllers/notifications.controllers.ts b/src/backend/src/controllers/notifications.controllers.ts index e85846405b..bdf9c44f13 100644 --- a/src/backend/src/controllers/notifications.controllers.ts +++ b/src/backend/src/controllers/notifications.controllers.ts @@ -11,34 +11,4 @@ export default class NotificationsController { next(error); } } - - static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) { - try { - const { organization, currentUser } = req; - - const unreadNotifications = await NotificationsService.getUserUnreadNotifications( - currentUser.userId, - organization.organizationId - ); - res.status(200).json(unreadNotifications); - } catch (error: unknown) { - next(error); - } - } - - static async removeUserNotification(req: Request, res: Response, next: NextFunction) { - try { - const { notificationId } = req.params; - const { organization, currentUser } = req; - - const unreadNotifications = await NotificationsService.removeUserNotification( - currentUser.userId, - notificationId, - organization.organizationId - ); - res.status(200).json(unreadNotifications); - } catch (error: unknown) { - next(error); - } - } } diff --git a/src/backend/src/controllers/popUps.controllers.ts b/src/backend/src/controllers/popUps.controllers.ts new file mode 100644 index 0000000000..6247a03fbd --- /dev/null +++ b/src/backend/src/controllers/popUps.controllers.ts @@ -0,0 +1,27 @@ +import { NextFunction, Request, Response } from 'express'; +import { PopUpService } from '../services/pop-up.services'; + +export default class PopUpsController { + static async getUserUnreadPopUps(req: Request, res: Response, next: NextFunction) { + try { + const { organization, currentUser } = req; + + const unreadPopUps = await PopUpService.getUserUnreadPopUps(currentUser.userId, organization.organizationId); + res.status(200).json(unreadPopUps); + } catch (error: unknown) { + next(error); + } + } + + static async removeUserPopUps(req: Request, res: Response, next: NextFunction) { + try { + const { popUpId } = req.params; + const { organization, currentUser } = req; + + const unreadPopUps = await PopUpService.removeUserPopUp(currentUser.userId, popUpId, organization.organizationId); + res.status(200).json(unreadPopUps); + } catch (error: unknown) { + next(error); + } + } +} diff --git a/src/backend/src/prisma-query-args/notifications.query-args.ts b/src/backend/src/prisma-query-args/notifications.query-args.ts deleted file mode 100644 index 4cf877ac5c..0000000000 --- a/src/backend/src/prisma-query-args/notifications.query-args.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Prisma } from '@prisma/client'; -import { getUserQueryArgs } from './user.query-args'; - -export type NotificationQueryArgs = ReturnType; - -export const getNotificationQueryArgs = (organizationId: string) => - Prisma.validator()({ - include: { - users: getUserQueryArgs(organizationId) - } - }); diff --git a/src/backend/src/prisma-query-args/pop-up.query-args.ts b/src/backend/src/prisma-query-args/pop-up.query-args.ts new file mode 100644 index 0000000000..0862956d22 --- /dev/null +++ b/src/backend/src/prisma-query-args/pop-up.query-args.ts @@ -0,0 +1,11 @@ +import { Prisma } from '@prisma/client'; +import { getUserQueryArgs } from './user.query-args'; + +export type PopUpQueryArgs = ReturnType; + +export const getPopUpQueryArgs = (organizationId: string) => + Prisma.validator()({ + include: { + users: getUserQueryArgs(organizationId) + } + }); diff --git a/src/backend/src/prisma/migrations/20241222123937_homepage_redesign/migration.sql b/src/backend/src/prisma/migrations/20241222150147_homepage_redesign/migration.sql similarity index 70% rename from src/backend/src/prisma/migrations/20241222123937_homepage_redesign/migration.sql rename to src/backend/src/prisma/migrations/20241222150147_homepage_redesign/migration.sql index 1ff99b84a0..17a36eb2d3 100644 --- a/src/backend/src/prisma/migrations/20241222123937_homepage_redesign/migration.sql +++ b/src/backend/src/prisma/migrations/20241222150147_homepage_redesign/migration.sql @@ -9,7 +9,7 @@ ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT; CREATE TABLE "Announcement" ( "announcementId" TEXT NOT NULL, "text" TEXT NOT NULL, - "dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "dateMessageSent" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "dateDeleted" TIMESTAMP(3), "senderName" TEXT NOT NULL, "slackEventId" TEXT NOT NULL, @@ -19,13 +19,13 @@ CREATE TABLE "Announcement" ( ); -- CreateTable -CREATE TABLE "Notification" ( - "notificationId" TEXT NOT NULL, +CREATE TABLE "PopUp" ( + "popUpId" TEXT NOT NULL, "text" TEXT NOT NULL, "iconName" TEXT NOT NULL, "eventLink" TEXT, - CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId") + CONSTRAINT "PopUp_pkey" PRIMARY KEY ("popUpId") ); -- CreateTable @@ -35,7 +35,7 @@ CREATE TABLE "_receivedAnnouncements" ( ); -- CreateTable -CREATE TABLE "_userNotifications" ( +CREATE TABLE "_userPopUps" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); @@ -50,10 +50,10 @@ CREATE UNIQUE INDEX "_receivedAnnouncements_AB_unique" ON "_receivedAnnouncement CREATE INDEX "_receivedAnnouncements_B_index" ON "_receivedAnnouncements"("B"); -- CreateIndex -CREATE UNIQUE INDEX "_userNotifications_AB_unique" ON "_userNotifications"("A", "B"); +CREATE UNIQUE INDEX "_userPopUps_AB_unique" ON "_userPopUps"("A", "B"); -- CreateIndex -CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B"); +CREATE INDEX "_userPopUps_B_index" ON "_userPopUps"("B"); -- AddForeignKey ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE SET NULL ON UPDATE CASCADE; @@ -65,7 +65,7 @@ ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_A_fk ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_userNotifications" ADD CONSTRAINT "_userNotifications_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_userPopUps" ADD CONSTRAINT "_userPopUps_A_fkey" FOREIGN KEY ("A") REFERENCES "PopUp"("popUpId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_userNotifications" ADD CONSTRAINT "_userNotifications_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_userPopUps" ADD CONSTRAINT "_userPopUps_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index 9d9bcb1f7f..c62827da19 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -181,7 +181,7 @@ model User { createdMilestones Milestone[] @relation(name: "milestoneCreator") deletedMilestones Milestone[] @relation(name: "milestoneDeleter") unreadAnnouncements Announcement[] @relation(name: "receivedAnnouncements") - unreadNotifications Notification[] @relation(name: "userNotifications") + unreadPopUps PopUp[] @relation(name: "userPopUps") } model Role { @@ -935,17 +935,17 @@ model Announcement { announcementId String @id @default(uuid()) text String usersReceived User[] @relation("receivedAnnouncements") - dateCreated DateTime @default(now()) + dateMessageSent DateTime @default(now()) dateDeleted DateTime? senderName String slackEventId String @unique slackChannelName String } -model Notification { - notificationId String @id @default(uuid()) - text String - iconName String - users User[] @relation("userNotifications") - eventLink String? +model PopUp { + popUpId String @id @default(uuid()) + text String + iconName String + users User[] @relation("userPopUps") + eventLink String? } diff --git a/src/backend/src/prisma/seed.ts b/src/backend/src/prisma/seed.ts index e7b5aa4c84..ddd4e190cb 100644 --- a/src/backend/src/prisma/seed.ts +++ b/src/backend/src/prisma/seed.ts @@ -1898,6 +1898,7 @@ const performSeed: () => Promise = async () => { await AnnouncementService.createAnnouncement( 'Welcome to Finishline!', [regina.userId], + new Date(), 'Thomas Emrax', '1', 'software', diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index d5411bddde..4701b0f3ea 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -4,7 +4,5 @@ import NotificationsController from '../controllers/notifications.controllers'; const notificationsRouter = express.Router(); notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications); -notificationsRouter.get('/current-user', NotificationsController.getUserUnreadNotifications); -notificationsRouter.post('/:notificationId/remove', NotificationsController.removeUserNotification); export default notificationsRouter; diff --git a/src/backend/src/routes/pop-up.routes.ts b/src/backend/src/routes/pop-up.routes.ts new file mode 100644 index 0000000000..5ecaeff01f --- /dev/null +++ b/src/backend/src/routes/pop-up.routes.ts @@ -0,0 +1,9 @@ +import express from 'express'; +import PopUpsController from '../controllers/popUps.controllers'; + +const popUpsRouter = express.Router(); + +popUpsRouter.get('/current-user', PopUpsController.getUserUnreadPopUps); +popUpsRouter.post('/:popUpId/remove', PopUpsController.removeUserPopUps); + +export default popUpsRouter; diff --git a/src/backend/src/services/announcement.service.ts b/src/backend/src/services/announcement.service.ts index 7562755f01..8d35cb879a 100644 --- a/src/backend/src/services/announcement.service.ts +++ b/src/backend/src/services/announcement.service.ts @@ -20,6 +20,7 @@ export default class AnnouncementService { static async createAnnouncement( text: string, usersReceivedIds: string[], + dateMessageSent: Date, senderName: string, slackEventId: string, slackChannelName: string, @@ -33,6 +34,7 @@ export default class AnnouncementService { userId: id })) }, + dateMessageSent, senderName, slackEventId, slackChannelName diff --git a/src/backend/src/services/change-requests.services.ts b/src/backend/src/services/change-requests.services.ts index 85e0dc5579..709f4b2e91 100644 --- a/src/backend/src/services/change-requests.services.ts +++ b/src/backend/src/services/change-requests.services.ts @@ -46,7 +46,7 @@ import { import { ChangeRequestQueryArgs, getChangeRequestQueryArgs } from '../prisma-query-args/change-requests.query-args'; import proposedSolutionTransformer from '../transformers/proposed-solutions.transformer'; import { getProposedSolutionQueryArgs } from '../prisma-query-args/proposed-solutions.query-args'; -import { sendHomeCrRequestReviewNotification, sendHomeCrReviewedNotification } from '../utils/notifications.utils'; +import { sendCrRequestReviewPopUp, sendCrReviewedPopUp } from '../utils/pop-up.utils'; export default class ChangeRequestsService { /** @@ -151,7 +151,7 @@ export default class ChangeRequestsService { // send a notification to the submitter that their change request has been reviewed await sendCRSubmitterReviewedNotification(updated); - await sendHomeCrReviewedNotification(foundCR, updated.submitter, accepted, organization.organizationId); + await sendCrReviewedPopUp(foundCR, updated.submitter, accepted, organization.organizationId); // send a reply to a CR's notifications of its updated status await sendSlackCRStatusToThread(updated.notificationSlackThreads, foundCR.crId, foundCR.identifier, accepted); @@ -1082,6 +1082,6 @@ export default class ChangeRequestsService { // send slack message to CR reviewers await sendSlackRequestedReviewNotification(newReviewers, changeRequestTransformer(foundCR)); - await sendHomeCrRequestReviewNotification(foundCR, newReviewers, organization.organizationId); + await sendCrRequestReviewPopUp(foundCR, newReviewers, organization.organizationId); } } diff --git a/src/backend/src/services/design-reviews.services.ts b/src/backend/src/services/design-reviews.services.ts index 644903fa43..64363b0c61 100644 --- a/src/backend/src/services/design-reviews.services.ts +++ b/src/backend/src/services/design-reviews.services.ts @@ -39,7 +39,7 @@ import { getWorkPackageQueryArgs } from '../prisma-query-args/work-packages.quer import { UserWithSettings } from '../utils/auth.utils'; import { getUserScheduleSettingsQueryArgs } from '../prisma-query-args/user.query-args'; import { createCalendarEvent, deleteCalendarEvent, updateCalendarEvent } from '../utils/google-integration.utils'; -import { sendHomeDrNotification } from '../utils/notifications.utils'; +import { sendDrPopUp } from '../utils/pop-up.utils'; export default class DesignReviewsService { /** @@ -206,7 +206,7 @@ export default class DesignReviewsService { } } - await sendHomeDrNotification(designReview, members, submitter, wbsElement.name, organization.organizationId); + await sendDrPopUp(designReview, members, submitter, wbsElement.name, organization.organizationId); const project = wbsElement.workPackage?.project; const teams = project?.teams; diff --git a/src/backend/src/services/notifications.services.ts b/src/backend/src/services/notifications.services.ts index 3da62b744f..a443d93588 100644 --- a/src/backend/src/services/notifications.services.ts +++ b/src/backend/src/services/notifications.services.ts @@ -11,10 +11,8 @@ import { daysBetween, startOfDay, wbsPipe } from 'shared'; import { buildDueString } from '../utils/slack.utils'; import WorkPackagesService from './work-packages.services'; import { addWeeksToDate } from 'shared'; -import { HttpException, NotFoundException } from '../utils/errors.utils'; +import { HttpException } from '../utils/errors.utils'; import { meetingStartTimePipe } from '../utils/design-reviews.utils'; -import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; -import notificationTransformer from '../transformers/notifications.transformer'; export default class NotificationsService { static async sendDailySlackNotifications() { @@ -195,98 +193,4 @@ export default class NotificationsService { await Promise.all(promises); } - - /** - * Gets all of a user's unread notifications - * @param userId id of user to get unread notifications from - * @param organization the user's orgainzation - * @returns the unread notifications of the user - */ - static async getUserUnreadNotifications(userId: string, organizationId: string) { - const unreadNotifications = await prisma.notification.findMany({ - where: { - users: { - some: { userId } - } - }, - ...getNotificationQueryArgs(organizationId) - }); - - if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found'); - - return unreadNotifications.map(notificationTransformer); - } - - /** - * Removes a notification from the user's unread notifications - * @param userId id of the current user - * @param notificationId id of the notification to remove - * @param organization the user's organization - * @returns the user's updated unread notifications - */ - static async removeUserNotification(userId: string, notificationId: string, organizationId: string) { - const updatedUser = await prisma.user.update({ - where: { userId }, - data: { - unreadNotifications: { - disconnect: { - notificationId - } - } - }, - include: { unreadNotifications: getNotificationQueryArgs(organizationId) } - }); - - if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`); - - return updatedUser.unreadNotifications.map(notificationTransformer); - } - - /** - * Creates and sends a notification to all users with the given userIds - * @param text writing in the notification - * @param iconName icon that appears in the notification - * @param userIds ids of users to send the notification to - * @param organizationId - * @param eventLink link the notification will go to when clicked - * @returns the created notification - */ - static async sendNotifcationToUsers( - text: string, - iconName: string, - userIds: string[], - organizationId: string, - eventLink?: string - ) { - const createdNotification = await prisma.notification.create({ - data: { - text, - iconName, - eventLink - }, - ...getNotificationQueryArgs(organizationId) - }); - - if (!createdNotification) throw new HttpException(500, 'Failed to create notification'); - - const notificationsPromises = userIds.map(async (userId) => { - const requestedUser = await prisma.user.findUnique({ - where: { userId } - }); - - if (!requestedUser) throw new NotFoundException('User', userId); - - return await prisma.user.update({ - where: { userId: requestedUser.userId }, - data: { - unreadNotifications: { - connect: { notificationId: createdNotification.notificationId } - } - } - }); - }); - - await Promise.all(notificationsPromises); - return notificationTransformer(createdNotification); - } } diff --git a/src/backend/src/services/pop-up.services.ts b/src/backend/src/services/pop-up.services.ts new file mode 100644 index 0000000000..267c72761f --- /dev/null +++ b/src/backend/src/services/pop-up.services.ts @@ -0,0 +1,100 @@ +import { getPopUpQueryArgs } from '../prisma-query-args/pop-up.query-args'; +import prisma from '../prisma/prisma'; +import popUpTransformer from '../transformers/pop-up.transformer'; +import { HttpException, NotFoundException } from '../utils/errors.utils'; + +export class PopUpService { + /** + * Gets all of a user's unread pop up + * @param userId id of user to get unread pop up from + * @param organization the user's orgainzation + * @returns the unread pop up of the user + */ + static async getUserUnreadPopUps(userId: string, organizationId: string) { + const unreadPopUps = await prisma.popUp.findMany({ + where: { + users: { + some: { userId } + } + }, + ...getPopUpQueryArgs(organizationId) + }); + + if (!unreadPopUps) throw new HttpException(404, 'User Unread Notifications Not Found'); + + return unreadPopUps.map(popUpTransformer); + } + + /** + * Removes a pop up from the user's unread pop up + * @param userId id of the current user + * @param popUpId id of the pop up to remove + * @param organization the user's organization + * @returns the user's updated unread pop up + */ + static async removeUserPopUp(userId: string, popUpId: string, organizationId: string) { + const updatedUser = await prisma.user.update({ + where: { userId }, + data: { + unreadPopUps: { + disconnect: { + popUpId + } + } + }, + include: { unreadPopUps: getPopUpQueryArgs(organizationId) } + }); + + if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${popUpId}`); + + return updatedUser.unreadPopUps.map(popUpTransformer); + } + + /** + * Creates and sends a pop up to all users with the given userIds + * @param text writing in the pop up + * @param iconName icon that appears in the pop up + * @param userIds ids of users to send the pop up to + * @param organizationId + * @param eventLink link the pop up will go to when clicked + * @returns the created notification + */ + static async sendPopUpToUsers( + text: string, + iconName: string, + userIds: string[], + organizationId: string, + eventLink?: string + ) { + const createdPopUp = await prisma.popUp.create({ + data: { + text, + iconName, + eventLink + }, + ...getPopUpQueryArgs(organizationId) + }); + + if (!createdPopUp) throw new HttpException(500, 'Failed to create notification'); + + const popUpPromises = userIds.map(async (userId) => { + const requestedUser = await prisma.user.findUnique({ + where: { userId } + }); + + if (!requestedUser) throw new NotFoundException('User', userId); + + return await prisma.user.update({ + where: { userId: requestedUser.userId }, + data: { + unreadPopUps: { + connect: { popUpId: createdPopUp.popUpId } + } + } + }); + }); + + await Promise.all(popUpPromises); + return popUpTransformer(createdPopUp); + } +} diff --git a/src/backend/src/transformers/notifications.transformer.ts b/src/backend/src/transformers/notifications.transformer.ts deleted file mode 100644 index 45dd25dee9..0000000000 --- a/src/backend/src/transformers/notifications.transformer.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Prisma } from '@prisma/client'; -import { NotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; -import { Notification } from 'shared'; - -const notificationTransformer = (notification: Prisma.NotificationGetPayload): Notification => { - return { - notificationId: notification.notificationId, - text: notification.text, - iconName: notification.iconName, - eventLink: notification.eventLink ?? undefined - }; -}; - -export default notificationTransformer; diff --git a/src/backend/src/transformers/pop-up.transformer.ts b/src/backend/src/transformers/pop-up.transformer.ts new file mode 100644 index 0000000000..1be7e4ec68 --- /dev/null +++ b/src/backend/src/transformers/pop-up.transformer.ts @@ -0,0 +1,12 @@ +import { Prisma } from '@prisma/client'; +import { PopUpQueryArgs } from '../prisma-query-args/pop-up.query-args'; +import { PopUp } from 'shared'; + +const popUpTransformer = (popUp: Prisma.PopUpGetPayload): PopUp => { + return { + ...popUp, + eventLink: popUp.eventLink ?? undefined + }; +}; + +export default popUpTransformer; diff --git a/src/backend/src/utils/auth.utils.ts b/src/backend/src/utils/auth.utils.ts index 7e7602eb6a..8520d881e4 100644 --- a/src/backend/src/utils/auth.utils.ts +++ b/src/backend/src/utils/auth.utils.ts @@ -66,7 +66,7 @@ export const requireJwtDev = (req: Request, res: Response, next: NextFunction) = ) { next(); } else if ( - req.path.startsWith('/notifications/taskdeadlines') // task deadline notification endpoint + req.path.startsWith('/notifications') // task deadline notification endpoint ) { notificationEndpointAuth(req, res, next); } else { diff --git a/src/backend/src/utils/notifications.utils.ts b/src/backend/src/utils/notifications.utils.ts index 445f36ed67..8fb6046318 100644 --- a/src/backend/src/utils/notifications.utils.ts +++ b/src/backend/src/utils/notifications.utils.ts @@ -1,7 +1,5 @@ -import { Task as Prisma_Task, WBS_Element, Design_Review, Change_Request } from '@prisma/client'; +import { Task as Prisma_Task, WBS_Element, Design_Review } from '@prisma/client'; import { UserWithSettings } from './auth.utils'; -import NotificationsService from '../services/notifications.services'; -import { User } from '@prisma/client'; export type TaskWithAssignees = Prisma_Task & { assignees: UserWithSettings[] | null; @@ -37,74 +35,3 @@ export const endOfDayTomorrow = () => { endOfDay.setDate(startOfDay.getDate() + 1); return endOfDay; }; - -/** - * Sends a finishline notification that a design review was scheduled - * @param designReview dr that was created - * @param members optional and required members of the dr - * @param submitter the user who created the dr - * @param workPackageName the name of the work package associated witht the dr - * @param organizationId id of the organization of the dr - */ -export const sendHomeDrNotification = async ( - designReview: Design_Review, - members: User[], - submitter: User, - workPackageName: string, - organizationId: string -) => { - const designReviewLink = `/settings/preferences?drId=${designReview.designReviewId}`; - - const msg = `Design Review for ${workPackageName} is being scheduled by ${submitter.firstName} ${submitter.lastName}`; - await NotificationsService.sendNotifcationToUsers( - msg, - 'calendar_month', - members.map((member) => member.userId), - organizationId, - designReviewLink - ); -}; - -/** - * Sends a finishline notification that a change request was reviewed - * @param changeRequest cr that was requested review - * @param submitter the user who submitted the cr - * @param accepted true if the cr changes were accepted, false if denied - * @param organizationId id of the organization of the cr - */ -export const sendHomeCrReviewedNotification = async ( - changeRequest: Change_Request, - submitter: User, - accepted: boolean, - organizationId: string -) => { - const changeRequestLink = `/change-requests/${changeRequest.crId}`; - await NotificationsService.sendNotifcationToUsers( - `CR #${changeRequest.identifier} has been ${accepted ? 'approved!' : 'denied.'}`, - accepted ? 'check_circle' : 'cancel', - [submitter.userId], - organizationId, - changeRequestLink - ); -}; - -/** - * Sends a finishline notification to all requested reviewers of a change request - * @param changeRequest cr that was requested review - * @param reviewers user's reviewing the cr - * @param organizationId id of the organization of the cr - */ -export const sendHomeCrRequestReviewNotification = async ( - changeRequest: Change_Request, - reviewers: User[], - organizationId: string -) => { - const changeRequestLink = `/change-requests/${changeRequest.crId}`; - await NotificationsService.sendNotifcationToUsers( - `Your review has been requested on CR #${changeRequest.identifier}`, - 'edit_note', - reviewers.map((reviewer) => reviewer.userId), - organizationId, - changeRequestLink - ); -}; diff --git a/src/backend/src/utils/pop-up.utils.ts b/src/backend/src/utils/pop-up.utils.ts new file mode 100644 index 0000000000..30c0fecdf0 --- /dev/null +++ b/src/backend/src/utils/pop-up.utils.ts @@ -0,0 +1,69 @@ +import { Change_Request, Design_Review, User } from '@prisma/client'; +import { PopUpService } from '../services/pop-up.services'; + +/** + * Sends a pop up that a design review was scheduled + * @param designReview dr that was created + * @param members optional and required members of the dr + * @param submitter the user who created the dr + * @param workPackageName the name of the work package associated witht the dr + * @param organizationId id of the organization of the dr + */ +export const sendDrPopUp = async ( + designReview: Design_Review, + members: User[], + submitter: User, + workPackageName: string, + organizationId: string +) => { + const designReviewLink = `/settings/preferences?drId=${designReview.designReviewId}`; + + const msg = `Design Review for ${workPackageName} is being scheduled by ${submitter.firstName} ${submitter.lastName}`; + await PopUpService.sendPopUpToUsers( + msg, + 'calendar_month', + members.map((member) => member.userId), + organizationId, + designReviewLink + ); +}; + +/** + * Sends a pop up that a change request was reviewed + * @param changeRequest cr that was requested review + * @param submitter the user who submitted the cr + * @param accepted true if the cr changes were accepted, false if denied + * @param organizationId id of the organization of the cr + */ +export const sendCrReviewedPopUp = async ( + changeRequest: Change_Request, + submitter: User, + accepted: boolean, + organizationId: string +) => { + const changeRequestLink = `/change-requests/${changeRequest.crId}`; + await PopUpService.sendPopUpToUsers( + `CR #${changeRequest.identifier} has been ${accepted ? 'approved!' : 'denied.'}`, + accepted ? 'check_circle' : 'cancel', + [submitter.userId], + organizationId, + changeRequestLink + ); +}; + +/** + * Sends a finishline pop up to all requested reviewers of a change request + * @param changeRequest cr that was requested review + * @param reviewers user's reviewing the cr + * @param organizationId id of the organization of the cr + */ +export const sendCrRequestReviewPopUp = async (changeRequest: Change_Request, reviewers: User[], organizationId: string) => { + const changeRequestLink = `/change-requests/${changeRequest.crId}`; + await PopUpService.sendPopUpToUsers( + `Your review has been requested on CR #${changeRequest.identifier}`, + 'edit_note', + reviewers.map((reviewer) => reviewer.userId), + organizationId, + changeRequestLink + ); +}; diff --git a/src/backend/tests/unmocked/announcements.test.ts b/src/backend/tests/unmocked/announcements.test.ts index 534b546edb..97ab4baeef 100644 --- a/src/backend/tests/unmocked/announcements.test.ts +++ b/src/backend/tests/unmocked/announcements.test.ts @@ -20,6 +20,7 @@ describe('Announcemnts Tests', () => { await AnnouncementService.createAnnouncement( 'test1', [testBatman.userId], + new Date(), 'Thomas Emrax', '1', 'software', @@ -28,6 +29,7 @@ describe('Announcemnts Tests', () => { await AnnouncementService.createAnnouncement( 'test2', [testBatman.userId], + new Date(), 'Superman', '50', 'mechanical', diff --git a/src/backend/tests/unmocked/notifications.test.ts b/src/backend/tests/unmocked/notifications.test.ts deleted file mode 100644 index 026099c1c3..0000000000 --- a/src/backend/tests/unmocked/notifications.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Organization } from '@prisma/client'; -import { createTestOrganization, createTestUser, resetUsers } from '../test-utils'; -import { batmanAppAdmin, supermanAdmin } from '../test-data/users.test-data'; -import { NotFoundException } from '../../src/utils/errors.utils'; -import prisma from '../../src/prisma/prisma'; -import NotificationService from '../../src/services/notifications.services'; - -describe('Notifications Tests', () => { - let orgId: string; - let organization: Organization; - beforeEach(async () => { - organization = await createTestOrganization(); - orgId = organization.organizationId; - }); - - afterEach(async () => { - await resetUsers(); - }); - - describe('Send Notification', () => { - it('fails on invalid user id', async () => { - await expect( - async () => - await NotificationService.sendNotifcationToUsers( - 'test notification', - 'star', - ['1', '2'], - organization.organizationId - ) - ).rejects.toThrow(new NotFoundException('User', '1')); - }); - - it('Succeeds and sends notification to user', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - const testSuperman = await createTestUser(supermanAdmin, orgId); - await NotificationService.sendNotifcationToUsers( - 'test notification', - 'star', - [testBatman.userId, testSuperman.userId], - organization.organizationId - ); - - const batmanWithNotifications = await prisma.user.findUnique({ - where: { userId: testBatman.userId }, - include: { unreadNotifications: true } - }); - - const supermanWithNotifications = await prisma.user.findUnique({ - where: { userId: testBatman.userId }, - include: { unreadNotifications: true } - }); - - expect(batmanWithNotifications?.unreadNotifications).toHaveLength(1); - expect(batmanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); - expect(supermanWithNotifications?.unreadNotifications).toHaveLength(1); - expect(supermanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); - }); - }); - - describe('Get Notifications', () => { - it('Succeeds and gets user notifications', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); - await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); - - const notifications = await NotificationService.getUserUnreadNotifications( - testBatman.userId, - organization.organizationId - ); - - expect(notifications).toHaveLength(2); - expect(notifications[0].text).toBe('test1'); - expect(notifications[1].text).toBe('test2'); - }); - }); - - describe('Remove Notifications', () => { - it('Succeeds and removes user notification', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId); - await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId); - - const notifications = await NotificationService.getUserUnreadNotifications( - testBatman.userId, - organization.organizationId - ); - - expect(notifications).toHaveLength(2); - expect(notifications[0].text).toBe('test1'); - expect(notifications[1].text).toBe('test2'); - - const updatedNotifications = await NotificationService.removeUserNotification( - testBatman.userId, - notifications[0].notificationId, - organization.organizationId - ); - - expect(updatedNotifications).toHaveLength(1); - expect(updatedNotifications[0].text).toBe('test2'); - }); - }); -}); diff --git a/src/backend/tests/unmocked/pop-up.test.ts b/src/backend/tests/unmocked/pop-up.test.ts new file mode 100644 index 0000000000..4421aea4f8 --- /dev/null +++ b/src/backend/tests/unmocked/pop-up.test.ts @@ -0,0 +1,90 @@ +import { Organization } from '@prisma/client'; +import { createTestOrganization, createTestUser, resetUsers } from '../test-utils'; +import { batmanAppAdmin, supermanAdmin } from '../test-data/users.test-data'; +import { NotFoundException } from '../../src/utils/errors.utils'; +import prisma from '../../src/prisma/prisma'; +import { PopUpService } from '../../src/services/pop-up.services'; + +describe('Pop Ups Tests', () => { + let orgId: string; + let organization: Organization; + beforeEach(async () => { + organization = await createTestOrganization(); + orgId = organization.organizationId; + }); + + afterEach(async () => { + await resetUsers(); + }); + + describe('Send Pop Up', () => { + it('fails on invalid user id', async () => { + await expect( + async () => await PopUpService.sendPopUpToUsers('test pop up', 'star', ['1', '2'], organization.organizationId) + ).rejects.toThrow(new NotFoundException('User', '1')); + }); + + it('Succeeds and sends pop up to user', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + const testSuperman = await createTestUser(supermanAdmin, orgId); + await PopUpService.sendPopUpToUsers( + 'test pop up', + 'star', + [testBatman.userId, testSuperman.userId], + organization.organizationId + ); + + const batmanWithPopUps = await prisma.user.findUnique({ + where: { userId: testBatman.userId }, + include: { unreadPopUps: true } + }); + + const supermanWithPopUps = await prisma.user.findUnique({ + where: { userId: testBatman.userId }, + include: { unreadPopUps: true } + }); + + expect(batmanWithPopUps?.unreadPopUps).toHaveLength(1); + expect(batmanWithPopUps?.unreadPopUps[0].text).toBe('test pop up'); + expect(supermanWithPopUps?.unreadPopUps).toHaveLength(1); + expect(supermanWithPopUps?.unreadPopUps[0].text).toBe('test pop up'); + }); + }); + + describe('Get Notifications', () => { + it('Succeeds and gets user pop ups', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await PopUpService.sendPopUpToUsers('test1', 'test1', [testBatman.userId], orgId); + await PopUpService.sendPopUpToUsers('test2', 'test2', [testBatman.userId], orgId); + + const popUps = await PopUpService.getUserUnreadPopUps(testBatman.userId, organization.organizationId); + + expect(popUps).toHaveLength(2); + expect(popUps[0].text).toBe('test1'); + expect(popUps[1].text).toBe('test2'); + }); + }); + + describe('Remove Pop Ups', () => { + it('Succeeds and removes user pop up', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + await PopUpService.sendPopUpToUsers('test1', 'test1', [testBatman.userId], orgId); + await PopUpService.sendPopUpToUsers('test2', 'test2', [testBatman.userId], orgId); + + const popUps = await PopUpService.getUserUnreadPopUps(testBatman.userId, organization.organizationId); + + expect(popUps).toHaveLength(2); + expect(popUps[0].text).toBe('test1'); + expect(popUps[1].text).toBe('test2'); + + const updatedPopUps = await PopUpService.removeUserPopUp( + testBatman.userId, + popUps[0].popUpId, + organization.organizationId + ); + + expect(updatedPopUps).toHaveLength(1); + expect(updatedPopUps[0].text).toBe('test2'); + }); + }); +}); diff --git a/src/frontend/src/apis/notifications.api.ts b/src/frontend/src/apis/notifications.api.ts deleted file mode 100644 index 454e9d4170..0000000000 --- a/src/frontend/src/apis/notifications.api.ts +++ /dev/null @@ -1,19 +0,0 @@ -import axios from '../utils/axios'; -import { apiUrls } from '../utils/urls'; -import { Notification } from 'shared'; - -/* - * Gets all unread notifications of the user with the given id - */ -export const getNotifications = () => { - return axios.get(apiUrls.notificationsCurrentUser(), { - transformResponse: (data) => JSON.parse(data) - }); -}; - -/* - * Removes a notification from the user with the given id - */ -export const removeNotification = (notificationId: string) => { - return axios.post(apiUrls.notificationsRemove(notificationId)); -}; diff --git a/src/frontend/src/apis/pop-ups.api.ts b/src/frontend/src/apis/pop-ups.api.ts new file mode 100644 index 0000000000..a0674b9527 --- /dev/null +++ b/src/frontend/src/apis/pop-ups.api.ts @@ -0,0 +1,19 @@ +import axios from '../utils/axios'; +import { apiUrls } from '../utils/urls'; +import { PopUp } from 'shared'; + +/* + * Gets all unread notifications of the user with the given id + */ +export const getPopUps = () => { + return axios.get(apiUrls.popUpsCurrentUser(), { + transformResponse: (data) => JSON.parse(data) + }); +}; + +/* + * Removes a notification from the user with the given id + */ +export const removePopUps = (notificationId: string) => { + return axios.post(apiUrls.popUpsRemove(notificationId)); +}; diff --git a/src/frontend/src/components/NotificationAlert.tsx b/src/frontend/src/components/NotificationAlert.tsx deleted file mode 100644 index 53b01a5aaa..0000000000 --- a/src/frontend/src/components/NotificationAlert.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Box } from '@mui/material'; -import React, { useEffect, useState } from 'react'; -import { Notification } from 'shared'; -import NotificationCard from './NotificationCard'; -import { useHistory } from 'react-router-dom'; -import { useCurrentUserNotifications, useRemoveUserNotification } from '../hooks/notifications.hooks'; - -const NotificationAlert: React.FC = () => { - const { data: notifications, isLoading: notificationsIsLoading } = useCurrentUserNotifications(); - const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification(); - const [currentNotification, setCurrentNotification] = useState(); - const history = useHistory(); - - useEffect(() => { - if (notifications && notifications.length > 0) { - setCurrentNotification(notifications[0]); - } - }, [notifications]); - - const removeNotificationWrapper = async (notification: Notification) => { - setCurrentNotification(undefined); - await removeNotification(notification); - }; - - const onClick = async (notification: Notification) => { - if (!!notification.eventLink) { - await removeNotificationWrapper(notification); - history.push(notification.eventLink); - } - }; - - return ( - - {!removeIsLoading && !notificationsIsLoading && currentNotification && ( - - )} - - ); -}; - -export default NotificationAlert; diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/NotificationCard.tsx index 38dd994029..e905ca2ad6 100644 --- a/src/frontend/src/components/NotificationCard.tsx +++ b/src/frontend/src/components/NotificationCard.tsx @@ -1,15 +1,15 @@ import { Box, Card, Icon, IconButton, Typography, useTheme } from '@mui/material'; import React from 'react'; -import { Notification } from 'shared'; +import { PopUp } from 'shared'; import CloseIcon from '@mui/icons-material/Close'; -interface NotificationCardProps { - notification: Notification; - removeNotification: (notificationId: Notification) => Promise; - onClick: (notificationId: Notification) => Promise; +interface PopUpCardProps { + popUp: PopUp; + removePopUp: (popUp: PopUp) => Promise; + onClick: (popUp: PopUp) => Promise; } -const NotificationCard: React.FC = ({ notification, removeNotification, onClick }) => { +const PopUpCard: React.FC = ({ popUp, removePopUp, onClick }) => { const theme = useTheme(); return ( = ({ notification, remov }} > await onClick(notification)} + onClick={async () => await onClick(popUp)} sx={{ display: 'flex', alignItems: 'center', gap: 1, - cursor: !!notification.eventLink ? 'pointer' : 'default' + cursor: !!popUp.eventLink ? 'pointer' : 'default' }} > = ({ notification, remov fontSize: 36 }} > - {notification.iconName} + {popUp.iconName} - {notification.text} + {popUp.text} - removeNotification(notification)}> + removePopUp(popUp)}> @@ -71,4 +71,4 @@ const NotificationCard: React.FC = ({ notification, remov ); }; -export default NotificationCard; +export default PopUpCard; diff --git a/src/frontend/src/components/PopUpAlert.tsx b/src/frontend/src/components/PopUpAlert.tsx new file mode 100644 index 0000000000..452f28800d --- /dev/null +++ b/src/frontend/src/components/PopUpAlert.tsx @@ -0,0 +1,49 @@ +import { Box } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { PopUp } from 'shared'; +import NotificationCard from './NotificationCard'; +import { useHistory } from 'react-router-dom'; +import { useCurrentUserPopUps, useRemoveUserPopUp } from '../hooks/pop-ups.hooks'; + +const PopUpAlert: React.FC = () => { + const { data: popUps, isLoading: popUpsIsLoading } = useCurrentUserPopUps(); + const { mutateAsync: removePopUp, isLoading: removeIsLoading } = useRemoveUserPopUp(); + const [currentPopUp, setCurrentPopUp] = useState(); + const history = useHistory(); + + useEffect(() => { + if (popUps && popUps.length > 0) { + setCurrentPopUp(popUps[0]); + } + }, [popUps]); + + const removePopUpWrapper = async (popUp: PopUp) => { + setCurrentPopUp(undefined); + await removePopUp(popUp); + }; + + const onClick = async (popUp: PopUp) => { + if (!!popUp.eventLink) { + await removePopUpWrapper(popUp); + history.push(popUp.eventLink); + } + }; + + return ( + + {!removeIsLoading && !popUpsIsLoading && currentPopUp && ( + + )} + + ); +}; + +export default PopUpAlert; diff --git a/src/frontend/src/hooks/notifications.hooks.ts b/src/frontend/src/hooks/notifications.hooks.ts deleted file mode 100644 index 5ca9ae621b..0000000000 --- a/src/frontend/src/hooks/notifications.hooks.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { Notification } from 'shared'; -import { getNotifications, removeNotification } from '../apis/notifications.api'; - -/** - * Curstom react hook to get all unread notifications from a user - * @param userId id of user to get unread notifications from - * @returns - */ -export const useCurrentUserNotifications = () => { - return useQuery(['notifications', 'current-user'], async () => { - const { data } = await getNotifications(); - return data; - }); -}; - -/** - * Curstom react hook to remove a notification from a user's unread notifications - * @param userId id of user to get unread notifications from - * @returns - */ -export const useRemoveUserNotification = () => { - const queryClient = useQueryClient(); - return useMutation( - ['notifications', 'current-user', 'remove'], - async (notification: Notification) => { - const { data } = await removeNotification(notification.notificationId); - return data; - }, - { - onSuccess: () => { - queryClient.invalidateQueries(['notifications', 'current-user']); - } - } - ); -}; diff --git a/src/frontend/src/hooks/pop-ups.hooks.ts b/src/frontend/src/hooks/pop-ups.hooks.ts new file mode 100644 index 0000000000..7816102dd6 --- /dev/null +++ b/src/frontend/src/hooks/pop-ups.hooks.ts @@ -0,0 +1,36 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { PopUp } from 'shared'; +import { getPopUps, removePopUps } from '../apis/pop-ups.api'; + +/** + * Curstom react hook to get all unread notifications from a user + * @param userId id of user to get unread notifications from + * @returns + */ +export const useCurrentUserPopUps = () => { + return useQuery(['pop-ups', 'current-user'], async () => { + const { data } = await getPopUps(); + return data; + }); +}; + +/** + * Curstom react hook to remove a notification from a user's unread notifications + * @param userId id of user to get unread notifications from + * @returns + */ +export const useRemoveUserPopUp = () => { + const queryClient = useQueryClient(); + return useMutation( + ['pop-ups', 'current-user', 'remove'], + async (popUp: PopUp) => { + const { data } = await removePopUps(popUp.popUpId); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['pop-ups', 'current-user']); + } + } + ); +}; diff --git a/src/frontend/src/pages/HomePage/Home.tsx b/src/frontend/src/pages/HomePage/Home.tsx index f76ecc0e4c..1ce8f7c7f6 100644 --- a/src/frontend/src/pages/HomePage/Home.tsx +++ b/src/frontend/src/pages/HomePage/Home.tsx @@ -11,14 +11,14 @@ import { useState } from 'react'; import MemberHomePage from './MemberHomePage'; import LeadHomePage from './LeadHomePage'; import AdminHomePage from './AdminHomePage'; -import NotificationAlert from '../../components/NotificationAlert'; +import PopUpAlert from '../../components/PopUpAlert'; const Home = () => { const user = useCurrentUser(); const [onMemberHomePage, setOnMemberHomePage] = useState(false); return ( <> - {!onMemberHomePage && } + {!onMemberHomePage && } {isGuest(user.role) && !onMemberHomePage ? ( ) : isMember(user.role) ? ( diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index a924917909..15a25d3026 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -197,10 +197,10 @@ const faqCreate = () => `${recruitment()}/faq/create`; const faqEdit = (id: string) => `${recruitment()}/faq/${id}/edit`; const faqDelete = (id: string) => `${recruitment()}/faq/${id}/delete`; -/************** Notification Endpoints ***************/ -const notifications = () => `${API_URL}/notifications`; -const notificationsCurrentUser = () => `${notifications()}/current-user`; -const notificationsRemove = (id: string) => `${notifications()}/${id}/remove`; +/************** Pop Up Endpoints ***************/ +const popUps = () => `${API_URL}/pop-ups`; +const popUpsCurrentUser = () => `${popUps()}/current-user`; +const popUpsRemove = (id: string) => `${popUps()}/${id}/remove`; /************** Onboarding Endpoints ***************/ const onboarding = () => `${API_URL}/onboarding`; @@ -367,9 +367,9 @@ export const apiUrls = { faqDelete, imageById, - notifications, - notificationsCurrentUser, - notificationsRemove, + popUps, + popUpsCurrentUser, + popUpsRemove, version }; diff --git a/src/shared/index.ts b/src/shared/index.ts index 40246d0fe4..93e9c8899b 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -11,7 +11,7 @@ export * from './src/types/team-types'; export * from './src/types/task-types'; export * from './src/types/reimbursement-requests-types'; export * from './src/types/design-review-types'; -export * from './src/types/notifications.types'; +export * from './src/types/pop-up-types'; export * from './src/types/announcements.types'; export * from './src/validate-wbs'; export * from './src/date-utils'; diff --git a/src/shared/src/types/announcements.types.ts b/src/shared/src/types/announcements.types.ts index c0e2d615a7..ac31f72062 100644 --- a/src/shared/src/types/announcements.types.ts +++ b/src/shared/src/types/announcements.types.ts @@ -5,7 +5,7 @@ export interface Announcement { text: string; usersReceived: User[]; senderName: string; - dateCreated: Date; + dateMessageSent: Date; slackEventId: string; slackChannelName: string; dateDeleted?: Date; diff --git a/src/shared/src/types/notifications.types.ts b/src/shared/src/types/pop-up-types.ts similarity index 50% rename from src/shared/src/types/notifications.types.ts rename to src/shared/src/types/pop-up-types.ts index abd16fcd21..2b517a3900 100644 --- a/src/shared/src/types/notifications.types.ts +++ b/src/shared/src/types/pop-up-types.ts @@ -1,5 +1,5 @@ -export interface Notification { - notificationId: string; +export interface PopUp { + popUpId: string; text: string; iconName: string; eventLink?: string; From dc35d6dce376caa9923b6ca9267a1811eae64ed0 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 22 Dec 2024 11:55:58 -0500 Subject: [PATCH 17/17] #3074-fixed pop up card --- src/frontend/src/components/PopUpAlert.tsx | 4 ++-- .../src/components/{NotificationCard.tsx => PopUpCard.tsx} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/frontend/src/components/{NotificationCard.tsx => PopUpCard.tsx} (100%) diff --git a/src/frontend/src/components/PopUpAlert.tsx b/src/frontend/src/components/PopUpAlert.tsx index 452f28800d..0a0b271b66 100644 --- a/src/frontend/src/components/PopUpAlert.tsx +++ b/src/frontend/src/components/PopUpAlert.tsx @@ -1,7 +1,7 @@ import { Box } from '@mui/material'; import React, { useEffect, useState } from 'react'; import { PopUp } from 'shared'; -import NotificationCard from './NotificationCard'; +import PopUpCard from './PopUpCard'; import { useHistory } from 'react-router-dom'; import { useCurrentUserPopUps, useRemoveUserPopUp } from '../hooks/pop-ups.hooks'; @@ -40,7 +40,7 @@ const PopUpAlert: React.FC = () => { }} > {!removeIsLoading && !popUpsIsLoading && currentPopUp && ( - + )} ); diff --git a/src/frontend/src/components/NotificationCard.tsx b/src/frontend/src/components/PopUpCard.tsx similarity index 100% rename from src/frontend/src/components/NotificationCard.tsx rename to src/frontend/src/components/PopUpCard.tsx