From 0f4c6b4f5747764b410480f4a88c9562c03f66a7 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Mon, 28 Oct 2024 22:13:14 -0400 Subject: [PATCH 01/32] #2821-getting all overdue work packages --- .../src/transformers/auth-user.transformer.ts | 4 +- .../src/pages/HomePage/AdminHomePage.tsx | 2 + .../src/pages/HomePage/LeadHomePage.tsx | 2 + .../components/OverdueWorkPackages.tsx | 57 +++++++++++++++++++ src/shared/src/types/user-types.ts | 4 +- 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx diff --git a/src/backend/src/transformers/auth-user.transformer.ts b/src/backend/src/transformers/auth-user.transformer.ts index 57ab1585c4..06bab93740 100644 --- a/src/backend/src/transformers/auth-user.transformer.ts +++ b/src/backend/src/transformers/auth-user.transformer.ts @@ -28,8 +28,8 @@ const authenticatedUserTransformer = ( changeRequestsToReviewId: user.changeRequestsToReview.map((changeRequest) => changeRequest.crId), organizations: user.organizations.map((organization) => organization.organizationId), currentOrganization: user.organizations.find((organization) => organization.organizationId === organizationId), - teamsAsHeadId: user.teamsAsHead.map(teamTransformer), - teamsAsLeadId: user.teamsAsLead.map(teamTransformer) + teamsAsHead: user.teamsAsHead.map(teamTransformer), + teamsAsLead: user.teamsAsLead.map(teamTransformer) }; }; diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index 4c1bf340f9..3b6ce04523 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -9,6 +9,7 @@ import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; import PageLayout from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; +import OverdueWorkPackages from './components/OverdueWorkPackages'; interface AdminHomePageProps { user: AuthenticatedUser; @@ -25,6 +26,7 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { Welcome, {user.firstName}! + ); }; diff --git a/src/frontend/src/pages/HomePage/LeadHomePage.tsx b/src/frontend/src/pages/HomePage/LeadHomePage.tsx index 76929deada..a0fd09864a 100644 --- a/src/frontend/src/pages/HomePage/LeadHomePage.tsx +++ b/src/frontend/src/pages/HomePage/LeadHomePage.tsx @@ -9,6 +9,7 @@ import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; import PageLayout from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; +import OverdueWorkPackages from './components/OverdueWorkPackages'; interface LeadHomePageProps { user: AuthenticatedUser; @@ -25,6 +26,7 @@ const LeadHomePage = ({ user }: LeadHomePageProps) => { Welcome, {user.firstName}! + ); }; diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx new file mode 100644 index 0000000000..0669d99ea0 --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { AuthenticatedUser, isAdmin, Team, WorkPackage } from 'shared'; +import ScrollablePageBlock from './ScrollablePageBlock'; +import { Box, Stack, useTheme } from '@mui/material'; +import WorkPackageCard from './WorkPackageCard'; +import { useAllWorkPackages, useGetManyWorkPackages } from '../../../hooks/work-packages.hooks'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; +import { daysOverdue } from '../../../utils/datetime.utils'; + +interface OverdueWorkPackagesViewProps { + workPackages: WorkPackage[]; +} + +interface OverdueWorkPackagesProps { + user: AuthenticatedUser; +} + +const getAllWbsNumFromTeams = (teams: Team[]) => { + const projects = teams.map((team) => team.projects).flat(); + const workPackages = projects.map((project) => project.workPackages).flat(); + return workPackages.map((wp) => wp.wbsNum); +}; + +const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { + return ( + + + {workPackages.map((wp) => ( + + ))} + + + ); +}; + +const OverdueWorkPackages: React.FC = ({ user }) => { + const teamsAsLeadership = [...user.teamsAsHead, ...user.teamsAsLead]; + const { data: allWps, isLoading: isLoadingAllWps, isError: isErrorAllWps, error: errorAllWps } = useAllWorkPackages(); + const { + data: teamWps, + isLoading: isLoadingTeamWps, + isError: isErrorTeamWps, + error: errorTeamWps + } = useGetManyWorkPackages(getAllWbsNumFromTeams(teamsAsLeadership)); + + if (isLoadingAllWps || isLoadingTeamWps || !allWps || !teamWps) return ; + if (isErrorAllWps) return ; + if (isErrorTeamWps) return ; + + const displayedWps = isAdmin(user.role) ? allWps : teamWps; + const overdueWps = displayedWps.filter((wp) => daysOverdue(wp.endDate) > 0); + + return ; +}; + +export default OverdueWorkPackages; diff --git a/src/shared/src/types/user-types.ts b/src/shared/src/types/user-types.ts index 79ca6c9c16..80d16f2537 100644 --- a/src/shared/src/types/user-types.ts +++ b/src/shared/src/types/user-types.ts @@ -65,8 +65,8 @@ export interface AuthenticatedUser { isAtLeastFinanceLead?: boolean; organizations: string[]; currentOrganization?: OrganizationPreview; - teamsAsHeadId: Team[]; - teamsAsLeadId: Team[]; + teamsAsHead: Team[]; + teamsAsLead: Team[]; } export interface UserSettings { From f8fc5adb3dadc3ea8f9ebbebeb1e127b09661c4b Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 5 Nov 2024 09:51:46 -0500 Subject: [PATCH 02/32] #2805-structure of page block --- .../components/OverdueWorkPackages.tsx | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx index 0669d99ea0..bfc3fe746d 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { AuthenticatedUser, isAdmin, Team, WorkPackage } from 'shared'; -import ScrollablePageBlock from './ScrollablePageBlock'; -import { Box, Stack, useTheme } from '@mui/material'; +import { Box, Card, CardContent, Stack, Typography, useTheme } from '@mui/material'; import WorkPackageCard from './WorkPackageCard'; import { useAllWorkPackages, useGetManyWorkPackages } from '../../../hooks/work-packages.hooks'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ErrorPage from '../../ErrorPage'; import { daysOverdue } from '../../../utils/datetime.utils'; +import { PAGE_GRID_HEIGHT } from '../../../components/PageLayout'; interface OverdueWorkPackagesViewProps { workPackages: WorkPackage[]; @@ -23,14 +23,56 @@ const getAllWbsNumFromTeams = (teams: Team[]) => { }; const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { + const theme = useTheme(); return ( - - - {workPackages.map((wp) => ( - - ))} - - + + + Overdue Work Packages + + + + + {workPackages.map((wp) => ( + + ))} + + + + ); }; From afab59ad838cb923cc7b2a350b1fb2ccf38b684b Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 6 Nov 2024 09:55:21 -0500 Subject: [PATCH 03/32] #2821-Working overdue wps section --- .../src/pages/HomePage/AdminHomePage.tsx | 10 ++++- .../components/OverdueWorkPackages.tsx | 42 +++++++++++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index 3b6ce04523..6d7f1c6b0b 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -3,7 +3,7 @@ * See the LICENSE file in the repository root folder for details. */ -import { Typography } from '@mui/material'; +import { Box, Grid, Typography } from '@mui/material'; import { useSingleUserSettings } from '../../hooks/users.hooks'; import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; @@ -26,7 +26,13 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { Welcome, {user.firstName}! - + + + + + + + ); }; diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx index bfc3fe746d..f6a227c9ff 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx @@ -7,6 +7,8 @@ import LoadingIndicator from '../../../components/LoadingIndicator'; import ErrorPage from '../../ErrorPage'; import { daysOverdue } from '../../../utils/datetime.utils'; import { PAGE_GRID_HEIGHT } from '../../../components/PageLayout'; +import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; +import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; interface OverdueWorkPackagesViewProps { workPackages: WorkPackage[]; @@ -16,6 +18,16 @@ interface OverdueWorkPackagesProps { user: AuthenticatedUser; } +const NoOverdueWPsDisplay: React.FC = () => { + return ( + } + heading={'Great Job Team!'} + message={'Your team has no overdue work packages!'} + /> + ); +}; + const getAllWbsNumFromTeams = (teams: Team[]) => { const projects = teams.map((team) => team.projects).flat(); const workPackages = projects.map((project) => project.workPackages).flat(); @@ -24,13 +36,18 @@ const getAllWbsNumFromTeams = (teams: Team[]) => { const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { const theme = useTheme(); + const isEmpty = workPackages.length === 0; return ( - + = ({ workP borderColor: theme.palette.primary.main }} > - Overdue Work Packages + + Overdue Work Packages + = ({ workP }} variant="outlined" > - - - {workPackages.map((wp) => ( - - ))} + + + {isEmpty ? : workPackages.map((wp) => )} From 609fb805c21f941b5ecfcb7a86b7a69d2cada57f Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 6 Nov 2024 10:04:05 -0500 Subject: [PATCH 04/32] #2821-optional teams as head --- src/shared/src/types/user-types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/src/types/user-types.ts b/src/shared/src/types/user-types.ts index 80d16f2537..2dc8fbb205 100644 --- a/src/shared/src/types/user-types.ts +++ b/src/shared/src/types/user-types.ts @@ -65,8 +65,8 @@ export interface AuthenticatedUser { isAtLeastFinanceLead?: boolean; organizations: string[]; currentOrganization?: OrganizationPreview; - teamsAsHead: Team[]; - teamsAsLead: Team[]; + teamsAsHead?: Team[]; + teamsAsLead?: Team[]; } export interface UserSettings { From 15ec8feaadafd6429be2b85dfa62892a5a5f315e Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 6 Nov 2024 10:10:02 -0500 Subject: [PATCH 05/32] #2821- check for undefined --- .../src/pages/HomePage/components/OverdueWorkPackages.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx index f6a227c9ff..fb9f5f801d 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx @@ -101,7 +101,9 @@ const OverdueWorkPackagesView: React.FC = ({ workP }; const OverdueWorkPackages: React.FC = ({ user }) => { - const teamsAsLeadership = [...user.teamsAsHead, ...user.teamsAsLead]; + const teamsAsHead = user.teamsAsHead ?? []; + const teamsAsLead = user.teamsAsLead ?? []; + const teamsAsLeadership = [...teamsAsHead, ...teamsAsLead]; const { data: allWps, isLoading: isLoadingAllWps, isError: isErrorAllWps, error: errorAllWps } = useAllWorkPackages(); const { data: teamWps, From 81e7ce2beb34afa7d984b6b5ce23f222c53e73a8 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 10 Nov 2024 11:19:29 -0500 Subject: [PATCH 06/32] #2769-added edit featured projects --- src/frontend/src/apis/organizations.api.ts | 6 ++ src/frontend/src/hooks/organizations.hooks.ts | 24 ++++- .../pages/AdminToolsPage/AdminToolsPage.tsx | 4 +- .../EditGuestView/EditFeaturedProjects.tsx | 99 +++++++++++++++++++ .../EditFeaturedProjectsDropdown.tsx | 39 ++++++++ .../EditFeaturedProjectsForm.tsx | 95 ++++++++++++++++++ .../EditGuestView/GuestViewConfig.tsx | 14 +++ src/frontend/src/utils/urls.ts | 2 + 8 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx create mode 100644 src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjectsDropdown.tsx create mode 100644 src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjectsForm.tsx create mode 100644 src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx diff --git a/src/frontend/src/apis/organizations.api.ts b/src/frontend/src/apis/organizations.api.ts index 486b22256a..e5903575c9 100644 --- a/src/frontend/src/apis/organizations.api.ts +++ b/src/frontend/src/apis/organizations.api.ts @@ -23,3 +23,9 @@ export const setOrganizationDescription = async (description: string) => { description }); }; + +export const setOrganizationFeaturedProjects = async (featuredProjectIds: string[]) => { + return axios.post(apiUrls.organizationsSetFeaturedProjects(), { + projectIds: featuredProjectIds + }); +}; diff --git a/src/frontend/src/hooks/organizations.hooks.ts b/src/frontend/src/hooks/organizations.hooks.ts index d182994fa2..24da9a5653 100644 --- a/src/frontend/src/hooks/organizations.hooks.ts +++ b/src/frontend/src/hooks/organizations.hooks.ts @@ -2,7 +2,12 @@ import { useContext, useState } from 'react'; import { OrganizationContext } from '../app/AppOrganizationContext'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import { Organization, Project } from 'shared'; -import { getFeaturedProjects, getCurrentOrganization, setOrganizationDescription } from '../apis/organizations.api'; +import { + getFeaturedProjects, + getCurrentOrganization, + setOrganizationDescription, + setOrganizationFeaturedProjects +} from '../apis/organizations.api'; interface OrganizationProvider { organizationId: string; @@ -54,7 +59,22 @@ export const useSetOrganizationDescription = () => { ['organizations', 'description'], async (description: string) => { const { data } = await setOrganizationDescription(description); - queryClient.invalidateQueries(['organizations']); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['organizations']); + } + } + ); +}; + +export const useSetFeaturedProjects = () => { + const queryClient = useQueryClient(); + return useMutation( + ['organizations', 'featured-projects'], + async (featuredProjects: Project[]) => { + const { data } = await setOrganizationFeaturedProjects(featuredProjects.map((project) => project.id)); return data; }, { diff --git a/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx b/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx index 04856d5265..653f6f7169 100644 --- a/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx +++ b/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx @@ -18,7 +18,7 @@ import NERTabs from '../../components/Tabs'; import { routes } from '../../utils/routes'; import { Box } from '@mui/system'; import AdminToolsRecruitmentConfig from './RecruitmentConfig/AdminToolsRecruitmentConfig'; -import EditDescription from './EditGuestView/EditDescription'; +import GuestViewConfig from './EditGuestView/GuestViewConfig'; const AdminToolsPage: React.FC = () => { const currentUser = useCurrentUser(); @@ -95,7 +95,7 @@ const AdminToolsPage: React.FC = () => { ) : tabIndex === 4 ? ( - + ) : ( diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx new file mode 100644 index 0000000000..cda841a38d --- /dev/null +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import EditFeaturedProjectsForm, { EditFeaturedProjectsFormInput } from './EditFeaturedProjectsForm'; +import { useFeaturedProjects, useSetFeaturedProjects } from '../../../hooks/organizations.hooks'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; +import { Box, Card, Chip, Typography, useTheme } from '@mui/material'; +import { NERButton } from '../../../components/NERButton'; +import { useToast } from '../../../hooks/toasts.hooks'; +import { projectWbsNamePipe } from '../../../utils/pipes'; + +const EditFeaturedProjects = () => { + const { data: featuredProjects, isLoading, isError, error } = useFeaturedProjects(); + const { mutateAsync: setFeaturedProjects } = useSetFeaturedProjects(); + const [isEditMode, setIsEditMode] = useState(false); + const theme = useTheme(); + const toast = useToast(); + + const handleClose = () => { + setIsEditMode(false); + }; + + const onSubmit = async (formInput: EditFeaturedProjectsFormInput) => { + try { + await setFeaturedProjects(formInput.featuredProjects); + toast.success('Featured Projects updated successfully!'); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + handleClose(); + }; + + if (isLoading || !featuredProjects) return ; + if (isError) return ; + + return ( + + + Featured Projects + + {isEditMode ? ( + + ) : ( + + + {featuredProjects.map((project) => ( + + ))} + + + setIsEditMode(true)}> + Update + + + + )} + + ); +}; + +export default EditFeaturedProjects; diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjectsDropdown.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjectsDropdown.tsx new file mode 100644 index 0000000000..254e776e29 --- /dev/null +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjectsDropdown.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Autocomplete, Chip, TextField } from '@mui/material'; +import { Project } from 'shared'; +import { projectWbsNamePipe } from '../../../utils/pipes'; +import { useAllProjects } from '../../../hooks/projects.hooks'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; + +interface EditFeatureProjectsDropdownProps { + onChange: (value: Project[] | null) => void; + value: Project[] | undefined; +} + +const EditFeaturedProjectsDropdown: React.FC = ({ onChange, value }) => { + const { data: allProjects, isLoading, isError, error } = useAllProjects(); + + if (isLoading || !allProjects) return ; + if (isError) return ; + + return ( + `${projectWbsNamePipe(option)}`} + isOptionEqualToValue={(option, value) => option.id === value.id} + value={value} + onChange={(_, newValue) => onChange(newValue)} + renderInput={(params) => } + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ( + + )) + } + placeholder={'Add a Project to Feature'} + /> + ); +}; + +export default EditFeaturedProjectsDropdown; diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjectsForm.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjectsForm.tsx new file mode 100644 index 0000000000..8d89a2da07 --- /dev/null +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjectsForm.tsx @@ -0,0 +1,95 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import EditFeaturedProjectsDropdown from './EditFeaturedProjectsDropdown'; +import { Box, FormControl } from '@mui/material'; +import { Project } from 'shared'; +import NERFailButton from '../../../components/NERFailButton'; +import NERSuccessButton from '../../../components/NERSuccessButton'; + +const schema = yup.object().shape({ + description: yup.array().of(yup.string()) +}); + +export interface EditFeaturedProjectsFormInput { + featuredProjects: Project[]; +} + +interface EditFeaturedProjectsFormProps { + featuredProjects: Project[]; + onSubmit: (formInput: EditFeaturedProjectsFormInput) => Promise; + onHide: () => void; + isEditMode: boolean; +} + +const EditFeaturedProjectsForm: React.FC = ({ + featuredProjects, + onSubmit, + onHide, + isEditMode +}) => { + const { handleSubmit, control, reset } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + featuredProjects: featuredProjects ?? [] + } + }); + + const onSubmitWrapper = async (data: EditFeaturedProjectsFormInput) => { + await onSubmit(data); + reset(); + }; + + const onHideWrapper = () => { + onHide(); + reset(); + }; + + return ( + + ); +}; + +export default EditFeaturedProjectsForm; diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx new file mode 100644 index 0000000000..7e47085d0f --- /dev/null +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx @@ -0,0 +1,14 @@ +import { Stack } from '@mui/material'; +import EditDescription from './EditDescription'; +import EditFeaturedProjects from './EditFeaturedProjects'; + +const GuestViewConfig: React.FC = () => { + return ( + + + + + ); +}; + +export default GuestViewConfig; diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 6908114edb..dfc667b83d 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -176,6 +176,7 @@ const organizationsUsefulLinks = () => `${organizations()}/useful-links`; const organizationsSetUsefulLinks = () => `${organizationsUsefulLinks()}/set`; const organizationsSetDescription = () => `${organizations()}/description/set`; const organizationsFeaturedProjects = () => `${organizations()}/featured-projects`; +const organizationsSetFeaturedProjects = () => `${organizationsFeaturedProjects()}/set`; /******************* Car Endpoints ********************/ const cars = () => `${API_URL}/cars`; @@ -334,6 +335,7 @@ export const apiUrls = { organizationsSetUsefulLinks, organizationsFeaturedProjects, organizationsSetDescription, + organizationsSetFeaturedProjects, cars, carsCreate, From 57784415625695f3aa2abad275dba21f97d9d2a7 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Mon, 18 Nov 2024 15:57:18 -0500 Subject: [PATCH 07/32] #2997-set up send notifcations endpoint --- .../src/controllers/users.controllers.ts | 12 ++++++++++++ src/backend/src/prisma/schema.prisma | 2 +- src/backend/src/routes/users.routes.ts | 6 ++++++ src/backend/src/services/users.services.ts | 19 +++++++++++++++++++ src/backend/tests/unmocked/users.test.ts | 17 +++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index 5cfa740368..efac029e88 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -191,4 +191,16 @@ export default class UsersController { return next(error); } } + + static async sendNotitifcation(req: Request, res: Response, next: NextFunction) { + try { + const { userId } = req.params; + const { text, iconName } = req.body; + + const updatedUser = await UsersService.sendNotification(userId, text, iconName); + return res.status(200).json(updatedUser); + } catch (error: unknown) { + return next(error); + } + } } diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index 112f7a6936..7023bbea12 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -944,5 +944,5 @@ model Notification { notificationId String @id @default(uuid()) text String iconName String - users User[] + usersReceived User[] } diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 2f95201f6f..99f7802b40 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,5 +54,11 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); +userRouter.post( + `/:userId/notifications/send`, + nonEmptyString(body('text')), + nonEmptyString(body('iconName')), + UsersController.sendNotitifcation +); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 4c7d2b4007..94867821bc 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -571,4 +571,23 @@ export default class UsersService { const resolvedTasks = await Promise.all(tasksPromises); return resolvedTasks.flat(); } + + static async sendNotification(userId: string, text: string, iconName: string) { + const createdNotification = await prisma.notification.create({ + data: { + text, + iconName + } + }); + + const udaptedUser = await prisma.user.update({ + where: { userId }, + data: { unreadNotifications: { connect: createdNotification } }, + include: { unreadNotifications: true } + }); + + if (!udaptedUser) throw new NotFoundException('User', userId); + + return udaptedUser; + } } diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index c13a0c857f..2b3e13d6df 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -48,4 +48,21 @@ describe('User Tests', () => { expect(userTasks).toStrictEqual([batmanTask, batmanTask]); }); }); + + describe('Send Notification', () => { + it('fails on invalid user id', async () => { + await expect(async () => await UsersService.sendNotification('1', 'test', 'test')).rejects.toThrow( + new NotFoundException('User', '1') + ); + }); + + /* + it('Succeeds and sends notification to user', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + UsersService.sendNotification(testBatman.userId, 'test', 'test'); + + expect(testBatman).toStrictEqual([batmanTask, batmanTask]); + }); + */ + }); }); From 83acfbebdf820057434b86d1f6ce700e1916a5fc Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Mon, 18 Nov 2024 21:36:01 -0500 Subject: [PATCH 08/32] #2997-created tests --- src/backend/src/services/users.services.ts | 10 +++++++--- src/backend/tests/unmocked/users.test.ts | 13 +++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 94867821bc..c617a642b1 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -573,6 +573,12 @@ export default class UsersService { } static async sendNotification(userId: string, text: string, iconName: string) { + const requestedUser = await prisma.user.findUnique({ + where: { userId } + }); + + if (!requestedUser) throw new NotFoundException('User', userId); + const createdNotification = await prisma.notification.create({ data: { text, @@ -581,13 +587,11 @@ export default class UsersService { }); const udaptedUser = await prisma.user.update({ - where: { userId }, + where: { userId: requestedUser.userId }, data: { unreadNotifications: { connect: createdNotification } }, include: { unreadNotifications: true } }); - if (!udaptedUser) throw new NotFoundException('User', userId); - return udaptedUser; } } diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 2b3e13d6df..f3115ae718 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -3,6 +3,7 @@ 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 prisma from '../../src/prisma/prisma'; describe('User Tests', () => { let orgId: string; @@ -56,13 +57,17 @@ describe('User Tests', () => { ); }); - /* it('Succeeds and sends notification to user', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); - UsersService.sendNotification(testBatman.userId, 'test', 'test'); + await UsersService.sendNotification(testBatman.userId, 'test1', 'test1'); + await UsersService.sendNotification(testBatman.userId, 'test2', 'test2'); - expect(testBatman).toStrictEqual([batmanTask, batmanTask]); + const batmanWithNotifications = await prisma.user.findUnique({ + where: { userId: testBatman.userId }, + include: { unreadNotifications: true } + }); + + expect(batmanWithNotifications?.unreadNotifications).toHaveLength(2); }); - */ }); }); From e3b3bb71727ba3a15459502baeca9116f3feeabd Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 19 Nov 2024 09:57:59 -0500 Subject: [PATCH 09/32] #2997-updated test --- src/backend/tests/unmocked/users.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index f3115ae718..b26a941bd5 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -68,6 +68,8 @@ describe('User Tests', () => { }); expect(batmanWithNotifications?.unreadNotifications).toHaveLength(2); + expect(batmanWithNotifications?.unreadNotifications[0].text).toBe('test1'); + expect(batmanWithNotifications?.unreadNotifications[1].text).toBe('test2'); }); }); }); From d2e9cdcf53cd7480d4553933d63c9f1851b6d72d Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 22 Nov 2024 10:24:57 -0500 Subject: [PATCH 10/32] #2821-separated into two files --- .../components/OverdueWorkPackageView.tsx | 88 +++++++++++++++++ .../components/OverdueWorkPackages.tsx | 97 +------------------ 2 files changed, 90 insertions(+), 95 deletions(-) create mode 100644 src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx new file mode 100644 index 0000000000..c80c4de9bc --- /dev/null +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -0,0 +1,88 @@ +import { Box, Card, CardContent, Stack, Typography, useTheme } from '@mui/material'; +import WorkPackageCard from './WorkPackageCard'; +import { PAGE_GRID_HEIGHT } from '../../../components/PageLayout'; +import { WorkPackage } from 'shared'; +import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; +import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; + +interface OverdueWorkPackagesViewProps { + workPackages: WorkPackage[]; +} + +const NoOverdueWPsDisplay: React.FC = () => { + return ( + } + heading={'Great Job Team!'} + message={'Your team has no overdue work packages!'} + /> + ); +}; + +const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { + const theme = useTheme(); + const isEmpty = workPackages.length === 0; + return ( + + + + Overdue Work Packages + + + + + + {isEmpty ? : workPackages.map((wp) => )} + + + + + ); +}; + +export default OverdueWorkPackagesView; diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx index f0fa8243e0..7e90c4e10a 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackages.tsx @@ -1,114 +1,21 @@ import React from 'react'; -import { AuthenticatedUser, isAdmin, Team, WorkPackage } from 'shared'; -import { Box, Card, CardContent, Stack, Typography, useTheme } from '@mui/material'; -import WorkPackageCard from './WorkPackageCard'; +import { AuthenticatedUser, isAdmin, Team } from 'shared'; import { useAllWorkPackages, useGetManyWorkPackages } from '../../../hooks/work-packages.hooks'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ErrorPage from '../../ErrorPage'; import { daysOverdue } from '../../../utils/datetime.utils'; -import { PAGE_GRID_HEIGHT } from '../../../components/PageLayout'; -import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; -import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; - -interface OverdueWorkPackagesViewProps { - workPackages: WorkPackage[]; -} +import OverdueWorkPackagesView from './OverdueWorkPackageView'; interface OverdueWorkPackagesProps { user: AuthenticatedUser; } -const NoOverdueWPsDisplay: React.FC = () => { - return ( - } - heading={'Great Job Team!'} - message={'Your team has no overdue work packages!'} - /> - ); -}; - const getAllWbsNumFromTeams = (teams: Team[]) => { const projects = teams.map((team) => team.projects).flat(); const workPackages = projects.map((project) => project.workPackages).flat(); return workPackages.map((wp) => wp.wbsNum); }; -const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { - const theme = useTheme(); - const isEmpty = workPackages.length === 0; - return ( - - - - Overdue Work Packages - - - - - - {isEmpty ? ( - - ) : ( - workPackages.map((wp) => ( - <> - - - - )) - )} - - - - - ); -}; - const OverdueWorkPackages: React.FC = ({ user }) => { const teamsAsHead = user.teamsAsHead ?? []; const teamsAsLead = user.teamsAsLead ?? []; From 93728debdb715088a34411789853509a693fd39a Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Fri, 22 Nov 2024 10:53:06 -0500 Subject: [PATCH 11/32] #2997-created migration --- .../migration.sql | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql diff --git a/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql b/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql new file mode 100644 index 0000000000..192243b385 --- /dev/null +++ b/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql @@ -0,0 +1,51 @@ +/* + Warnings: + + - You are about to drop the column `userId` on the `Notification` table. All the data in the column will be lost. + - Added the required column `text` to the `Announcement` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Notification" DROP CONSTRAINT "Notification_userId_fkey"; + +-- AlterTable +ALTER TABLE "Announcement" ADD COLUMN "text" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Notification" DROP COLUMN "userId"; + +-- CreateTable +CREATE TABLE "_ReceivedAnnouncements" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_NotificationToUser" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_NotificationToUser_AB_unique" ON "_NotificationToUser"("A", "B"); + +-- CreateIndex +CREATE INDEX "_NotificationToUser_B_index" ON "_NotificationToUser"("B"); + +-- AddForeignKey +ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; From 45d0574e5e7bba5483807f3d9b6c329f2cfce3d7 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 26 Nov 2024 17:38:03 -0500 Subject: [PATCH 12/32] #2997-creates only one notifcation and sends to multiple users --- .../src/controllers/users.controllers.ts | 12 ------- .../notifications.query-args.ts | 11 ++++++ src/backend/src/routes/users.routes.ts | 6 ---- src/backend/src/services/users.services.ts | 23 ------------ .../transformers/notification.transformer.ts | 13 +++++++ .../src/utils/homepage-notifications.utils.ts | 36 +++++++++++++++++++ src/backend/tests/unmocked/users.test.ts | 2 ++ src/shared/index.ts | 1 + src/shared/src/types/notification.types.ts | 5 +++ 9 files changed, 68 insertions(+), 41 deletions(-) create mode 100644 src/backend/src/prisma-query-args/notifications.query-args.ts create mode 100644 src/backend/src/transformers/notification.transformer.ts create mode 100644 src/backend/src/utils/homepage-notifications.utils.ts create mode 100644 src/shared/src/types/notification.types.ts diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index efac029e88..5cfa740368 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -191,16 +191,4 @@ export default class UsersController { return next(error); } } - - static async sendNotitifcation(req: Request, res: Response, next: NextFunction) { - try { - const { userId } = req.params; - const { text, iconName } = req.body; - - const updatedUser = await UsersService.sendNotification(userId, text, iconName); - return res.status(200).json(updatedUser); - } catch (error: unknown) { - return 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 new file mode 100644 index 0000000000..9081f717cc --- /dev/null +++ b/src/backend/src/prisma-query-args/notifications.query-args.ts @@ -0,0 +1,11 @@ +import { Prisma } from '@prisma/client'; +import { getUserQueryArgs } from './user.query-args'; + +export type NotificationQueryArgs = ReturnType; + +export const getNotificationQueryArgs = (organizationId: string) => + Prisma.validator()({ + include: { + usersReceived: getUserQueryArgs(organizationId) + } + }); diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 99f7802b40..2f95201f6f 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,11 +54,5 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); -userRouter.post( - `/:userId/notifications/send`, - nonEmptyString(body('text')), - nonEmptyString(body('iconName')), - UsersController.sendNotitifcation -); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index c617a642b1..4c7d2b4007 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -571,27 +571,4 @@ export default class UsersService { const resolvedTasks = await Promise.all(tasksPromises); return resolvedTasks.flat(); } - - static async sendNotification(userId: string, text: string, iconName: string) { - const requestedUser = await prisma.user.findUnique({ - where: { userId } - }); - - if (!requestedUser) throw new NotFoundException('User', userId); - - const createdNotification = await prisma.notification.create({ - data: { - text, - iconName - } - }); - - const udaptedUser = await prisma.user.update({ - where: { userId: requestedUser.userId }, - data: { unreadNotifications: { connect: createdNotification } }, - include: { unreadNotifications: true } - }); - - return udaptedUser; - } } diff --git a/src/backend/src/transformers/notification.transformer.ts b/src/backend/src/transformers/notification.transformer.ts new file mode 100644 index 0000000000..32666b151a --- /dev/null +++ b/src/backend/src/transformers/notification.transformer.ts @@ -0,0 +1,13 @@ +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 + }; +}; + +export default notificationTransformer; diff --git a/src/backend/src/utils/homepage-notifications.utils.ts b/src/backend/src/utils/homepage-notifications.utils.ts new file mode 100644 index 0000000000..d823098da8 --- /dev/null +++ b/src/backend/src/utils/homepage-notifications.utils.ts @@ -0,0 +1,36 @@ +import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; +import prisma from '../prisma/prisma'; +import notificationTransformer from '../transformers/notification.transformer'; +import { NotFoundException } from './errors.utils'; + +const sendNotificationToUser = async (userId: string, notificationId: string, organizationId: string) => { + const requestedUser = await prisma.user.findUnique({ + where: { userId } + }); + + if (!requestedUser) throw new NotFoundException('User', userId); + + const updatedUser = await prisma.user.update({ + where: { userId: requestedUser.userId }, + data: { unreadNotifications: { connect: { notificationId } } }, + include: { unreadNotifications: getNotificationQueryArgs(organizationId) } + }); + + return updatedUser.unreadNotifications.map(notificationTransformer); +}; + +export const sendNotificationToUsers = async (userIds: string[], text: string, iconName: string, organizationId: string) => { + const createdNotification = await prisma.notification.create({ + data: { + text, + iconName + } + }); + + const notificationPromises = userIds.map(async (userId) => { + return sendNotificationToUser(userId, createdNotification.notificationId, organizationId); + }); + + const resolvedNotifications = await Promise.all(notificationPromises); + return resolvedNotifications.flat(); +}; diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index b26a941bd5..dcf7ab3e3c 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -50,6 +50,7 @@ describe('User Tests', () => { }); }); + /* describe('Send Notification', () => { it('fails on invalid user id', async () => { await expect(async () => await UsersService.sendNotification('1', 'test', 'test')).rejects.toThrow( @@ -72,4 +73,5 @@ describe('User Tests', () => { expect(batmanWithNotifications?.unreadNotifications[1].text).toBe('test2'); }); }); + */ }); diff --git a/src/shared/index.ts b/src/shared/index.ts index 1d3f6c399f..763ce5e09e 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -11,6 +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/notification.types'; export * from './src/validate-wbs'; export * from './src/date-utils'; diff --git a/src/shared/src/types/notification.types.ts b/src/shared/src/types/notification.types.ts new file mode 100644 index 0000000000..e4419ef2ed --- /dev/null +++ b/src/shared/src/types/notification.types.ts @@ -0,0 +1,5 @@ +export interface Notification { + notificationId: string; + text: string; + iconName: string; +} From 598d8fe9e58e6ab9d83bcaef37c154eedfe46eeb Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 26 Nov 2024 20:25:41 -0500 Subject: [PATCH 13/32] created tests for sending notifications --- .../src/utils/homepage-notifications.utils.ts | 7 +-- .../tests/unmocked/notifications.test.ts | 49 +++++++++++++++++++ src/backend/tests/unmocked/users.test.ts | 26 ---------- 3 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 src/backend/tests/unmocked/notifications.test.ts diff --git a/src/backend/src/utils/homepage-notifications.utils.ts b/src/backend/src/utils/homepage-notifications.utils.ts index d823098da8..f795064c28 100644 --- a/src/backend/src/utils/homepage-notifications.utils.ts +++ b/src/backend/src/utils/homepage-notifications.utils.ts @@ -24,13 +24,14 @@ export const sendNotificationToUsers = async (userIds: string[], text: string, i data: { text, iconName - } + }, + ...getNotificationQueryArgs(organizationId) }); const notificationPromises = userIds.map(async (userId) => { return sendNotificationToUser(userId, createdNotification.notificationId, organizationId); }); - const resolvedNotifications = await Promise.all(notificationPromises); - return resolvedNotifications.flat(); + await Promise.all(notificationPromises); + return notificationTransformer(createdNotification); }; diff --git a/src/backend/tests/unmocked/notifications.test.ts b/src/backend/tests/unmocked/notifications.test.ts new file mode 100644 index 0000000000..eea07b47e4 --- /dev/null +++ b/src/backend/tests/unmocked/notifications.test.ts @@ -0,0 +1,49 @@ +import { Organization } from '@prisma/client'; +import { createTestOrganization, createTestUser, resetUsers } from '../test-utils'; +import { batmanAppAdmin, wonderwomanGuest } from '../test-data/users.test-data'; +import { NotFoundException } from '../../src/utils/errors.utils'; +import { sendNotificationToUsers } from '../../src/utils/homepage-notifications.utils'; +import prisma from '../../src/prisma/prisma'; + +describe('Notification 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 sendNotificationToUsers(['1'], 'test', 'test', orgId)).rejects.toThrow( + new NotFoundException('User', '1') + ); + }); + + it('Succeeds and sends notification to user', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + const testWonderWoman = await createTestUser(wonderwomanGuest, orgId); + + const notification = await sendNotificationToUsers([testBatman.userId, testWonderWoman.userId], 'test', 'icon', orgId); + + const batmanWithNotifications = await prisma.user.findUnique({ + where: { userId: testBatman.userId }, + include: { unreadNotifications: true } + }); + const wonderWomanWithNotifications = await prisma.user.findUnique({ + where: { userId: testWonderWoman.userId }, + include: { unreadNotifications: true } + }); + + expect(batmanWithNotifications?.unreadNotifications).toHaveLength(1); + expect(batmanWithNotifications?.unreadNotifications[0]).toStrictEqual(notification); + + expect(wonderWomanWithNotifications?.unreadNotifications).toHaveLength(1); + expect(wonderWomanWithNotifications?.unreadNotifications[0]).toStrictEqual(notification); + }); + }); +}); diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index dcf7ab3e3c..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 prisma from '../../src/prisma/prisma'; describe('User Tests', () => { let orgId: string; @@ -49,29 +48,4 @@ describe('User Tests', () => { expect(userTasks).toStrictEqual([batmanTask, batmanTask]); }); }); - - /* - describe('Send Notification', () => { - it('fails on invalid user id', async () => { - await expect(async () => await UsersService.sendNotification('1', 'test', 'test')).rejects.toThrow( - new NotFoundException('User', '1') - ); - }); - - it('Succeeds and sends notification to user', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await UsersService.sendNotification(testBatman.userId, 'test1', 'test1'); - await UsersService.sendNotification(testBatman.userId, 'test2', 'test2'); - - const batmanWithNotifications = await prisma.user.findUnique({ - where: { userId: testBatman.userId }, - include: { unreadNotifications: true } - }); - - expect(batmanWithNotifications?.unreadNotifications).toHaveLength(2); - expect(batmanWithNotifications?.unreadNotifications[0].text).toBe('test1'); - expect(batmanWithNotifications?.unreadNotifications[1].text).toBe('test2'); - }); - }); - */ }); From dc18abe55d2eb8709f39d1da1ffa3020eba6d75d Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 11:29:06 -0500 Subject: [PATCH 14/32] #2821-reversed changes to card --- .../src/pages/HomePage/AdminHomePage.tsx | 10 +- .../HomePage/components/WorkPackageCard.tsx | 130 +++++++++++++++--- 2 files changed, 113 insertions(+), 27 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index 6d7f1c6b0b..ee4b3e08cc 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -26,13 +26,11 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { Welcome, {user.firstName}! - - - - - + + + - + ); }; diff --git a/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx b/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx index 57814b4f3f..14557f015f 100644 --- a/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx +++ b/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx @@ -6,15 +6,19 @@ import { Chip, CircularProgress, CircularProgressProps, + Grid, Link, Stack, Typography, useTheme } from '@mui/material'; import { wbsPipe, WorkPackage } from 'shared'; -import { datePipe, fullNamePipe, projectWbsPipe } from '../../../utils/pipes'; +import { datePipe, fullNamePipe, projectWbsPipe, wbsNamePipe } from '../../../utils/pipes'; import { routes } from '../../../utils/routes'; import { Link as RouterLink } from 'react-router-dom'; +import { daysOverdue } from '../../../utils/datetime.utils'; +import { useGetManyWorkPackages } from '../../../hooks/work-packages.hooks'; +import LoadingIndicator from '../../../components/LoadingIndicator'; export const CircularProgressWithLabel = (props: CircularProgressProps & { value: number }) => { return ( @@ -36,6 +40,29 @@ export const CircularProgressWithLabel = (props: CircularProgressProps & { value const WorkPackageCard = ({ wp }: { wp: WorkPackage }) => { const theme = useTheme(); + const { data: blockedByWps, isLoading } = useGetManyWorkPackages(wp.blockedBy); + const numDaysOverdue = daysOverdue(new Date(wp.endDate)); + const isOverdue = numDaysOverdue > 0; + if (isLoading || !blockedByWps) return ; + + const WpChipDisplay = ({ wp, isOverdue }: { wp: WorkPackage; isOverdue: boolean }) => { + const chipSize = isOverdue ? 'small' : 'medium'; + return ( + + } label={fullNamePipe(wp.lead)} size={chipSize} /> + } label={fullNamePipe(wp.manager)} size={chipSize} /> + } label={'TEAM'} size={chipSize} /> + + ); + }; + return ( { }} > - - - - - {projectWbsPipe(wp.wbsNum)} - {wp.projectName} - - - - - {wbsPipe(wp.wbsNum)} - {wp.name} + + + + + + {wbsPipe(wp.wbsNum)} - {wp.name} + + + + + {projectWbsPipe(wp.wbsNum)} - {wp.projectName} + + + {!isOverdue && ( + + {datePipe(wp.startDate) + ' ⟝ ' + wp.duration + ' wks ⟞ ' + datePipe(wp.endDate)} + + )} + + Blocked By: - - - {datePipe(wp.startDate) + ' ⟝ ' + wp.duration + ' wks ⟞ ' + datePipe(wp.endDate)} - - - - - } label={fullNamePipe(wp.lead)} size="medium" /> - } label={fullNamePipe(wp.manager)} size="medium" /> - +
    + {blockedByWps.length === 0 ? ( +
  • + + No Blockers + +
  • + ) : ( + blockedByWps.map((wp) => ( +
  • + + {wbsNamePipe(wp)} + +
  • + )) + )} +
+ {!isOverdue && } + + + {isOverdue && ( + + + + + + {numDaysOverdue} + + + + Days + + + Overdue + + + + + + )} +
); From a987afc41c5cf5c5e0061d552aadbb66dfc9e7a1 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 11:44:37 -0500 Subject: [PATCH 15/32] #2821-fixed changes on wp card --- .../HomePage/components/WorkPackageCard.tsx | 130 +++--------------- 1 file changed, 21 insertions(+), 109 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx b/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx index 7fcb42f29c..5419731678 100644 --- a/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx +++ b/src/frontend/src/pages/HomePage/components/WorkPackageCard.tsx @@ -6,19 +6,15 @@ import { Chip, CircularProgress, CircularProgressProps, - Grid, Link, Stack, Typography, useTheme } from '@mui/material'; import { wbsPipe, WorkPackage } from 'shared'; -import { datePipe, fullNamePipe, projectWbsPipe, wbsNamePipe } from '../../../utils/pipes'; +import { datePipe, fullNamePipe, projectWbsPipe } from '../../../utils/pipes'; import { routes } from '../../../utils/routes'; import { Link as RouterLink } from 'react-router-dom'; -import { daysOverdue } from '../../../utils/datetime.utils'; -import { useGetManyWorkPackages } from '../../../hooks/work-packages.hooks'; -import LoadingIndicator from '../../../components/LoadingIndicator'; export const CircularProgressWithLabel = (props: CircularProgressProps & { value: number }) => { return ( @@ -40,29 +36,6 @@ export const CircularProgressWithLabel = (props: CircularProgressProps & { value const WorkPackageCard = ({ wp }: { wp: WorkPackage }) => { const theme = useTheme(); - const { data: blockedByWps, isLoading } = useGetManyWorkPackages(wp.blockedBy); - const numDaysOverdue = daysOverdue(new Date(wp.endDate)); - const isOverdue = numDaysOverdue > 0; - if (isLoading || !blockedByWps) return ; - - const WpChipDisplay = ({ wp, isOverdue }: { wp: WorkPackage; isOverdue: boolean }) => { - const chipSize = isOverdue ? 'small' : 'medium'; - return ( - - } label={fullNamePipe(wp.lead)} size={chipSize} /> - } label={fullNamePipe(wp.manager)} size={chipSize} /> - } label={'TEAM'} size={chipSize} /> - - ); - }; - return ( { }} > - - - - - - {wbsPipe(wp.wbsNum)} - {wp.name} - - - - - {projectWbsPipe(wp.wbsNum)} - {wp.projectName} - - - {!isOverdue && ( - - {datePipe(wp.startDate) + ' ⟝ ' + wp.duration + ' wks ⟞ ' + datePipe(wp.endDate)} - - )} - - Blocked By: + + + + + {projectWbsPipe(wp.wbsNum)} - {wp.projectName} + + + + + {wbsPipe(wp.wbsNum)} - {wp.name} -
    - {blockedByWps.length === 0 ? ( -
  • - - No Blockers - -
  • - ) : ( - blockedByWps.map((wp) => ( -
  • - - {wbsNamePipe(wp)} - -
  • - )) - )} -
- {!isOverdue && } -
-
- {isOverdue && ( - - - - - - {numDaysOverdue} - - - - Days - - - Overdue - - - - - - )} -
+ + + {datePipe(wp.startDate) + ' ⟝ ' + wp.duration + ' wks ⟞ ' + datePipe(wp.endDate)} + +
+ + + } label={fullNamePipe(wp.lead)} size="medium" /> + } label={fullNamePipe(wp.manager)} size="medium" /> + ); From 6e8bc4be4284164f37eda1d8680d247a4a2d23f2 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 11:54:30 -0500 Subject: [PATCH 16/32] #2821-Added to admin homepage --- .../src/pages/HomePage/AdminHomePage.tsx | 28 ++++++++++++++----- .../components/OverdueWorkPackageView.tsx | 11 +++----- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index 4dc62858e0..b83e32a6ab 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -3,7 +3,7 @@ * See the LICENSE file in the repository root folder for details. */ -import { Typography, Grid } from '@mui/material'; +import { Typography, Grid, Box } from '@mui/material'; import { useSingleUserSettings } from '../../hooks/users.hooks'; import LoadingIndicator from '../../components/LoadingIndicator'; import ErrorPage from '../ErrorPage'; @@ -11,6 +11,7 @@ import PageLayout, { PAGE_GRID_HEIGHT } from '../../components/PageLayout'; import { AuthenticatedUser } from 'shared'; import WorkPackagesSelectionView from './components/WorkPackagesSelectionView'; import ChangeRequestsToReview from './components/ChangeRequestsToReview'; +import OverdueWorkPackages from './components/OverdueWorkPackages'; interface AdminHomePageProps { user: AuthenticatedUser; @@ -27,14 +28,27 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { Welcome, {user.firstName}! - - + + + + + + + + + + - - - - +
); }; diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index c80c4de9bc..2dba485587 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -1,6 +1,5 @@ import { Box, Card, CardContent, Stack, Typography, useTheme } from '@mui/material'; import WorkPackageCard from './WorkPackageCard'; -import { PAGE_GRID_HEIGHT } from '../../../components/PageLayout'; import { WorkPackage } from 'shared'; import EmptyPageBlockDisplay from './EmptyPageBlockDisplay'; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; @@ -21,9 +20,8 @@ const NoOverdueWPsDisplay: React.FC = () => { const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { const theme = useTheme(); - const isEmpty = workPackages.length === 0; return ( - + = ({ workP > - - {isEmpty ? : workPackages.map((wp) => )} + + {workPackages.length === 0 ? : workPackages.map((wp) => )} From 911723737435b2d6e41b3bd456dc9a488808f9a1 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 13:15:48 -0500 Subject: [PATCH 17/32] #2821-fixed spacings --- .../src/pages/HomePage/AdminHomePage.tsx | 4 +- .../components/OverdueWorkPackageView.tsx | 40 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index b83e32a6ab..edee46bbab 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -41,10 +41,10 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { - + - + diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index 2dba485587..24a48cf2f6 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -21,18 +21,18 @@ const NoOverdueWPsDisplay: React.FC = () => { const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { const theme = useTheme(); return ( - + = ({ workP = ({ workP > - + {workPackages.length === 0 ? : workPackages.map((wp) => )} From 8f609281e17f8fcdb77f7715ad144f23f53e56b1 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 13:17:28 -0500 Subject: [PATCH 18/32] #2821-fixed margins on empty --- .../src/pages/HomePage/components/OverdueWorkPackageView.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index 24a48cf2f6..b3621c742c 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -20,6 +20,7 @@ const NoOverdueWPsDisplay: React.FC = () => { const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { const theme = useTheme(); + const isEmpty = workPackages.length === 0; return ( = ({ workP > = ({ workP height: '100%' }} > - {workPackages.length === 0 ? : workPackages.map((wp) => )} + {isEmpty ? : workPackages.map((wp) => )} From afd33a088198a895930069d42e2c239713adbcdc Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 13:53:34 -0500 Subject: [PATCH 19/32] #2821-reverted updates --- .../src/pages/HomePage/AdminHomePage.tsx | 4 +- .../components/OverdueWorkPackageView.tsx | 43 +++++++++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/frontend/src/pages/HomePage/AdminHomePage.tsx b/src/frontend/src/pages/HomePage/AdminHomePage.tsx index b83e32a6ab..edee46bbab 100644 --- a/src/frontend/src/pages/HomePage/AdminHomePage.tsx +++ b/src/frontend/src/pages/HomePage/AdminHomePage.tsx @@ -41,10 +41,10 @@ const AdminHomePage = ({ user }: AdminHomePageProps) => { - + - + diff --git a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx index 2dba485587..b3621c742c 100644 --- a/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx +++ b/src/frontend/src/pages/HomePage/components/OverdueWorkPackageView.tsx @@ -20,19 +20,20 @@ const NoOverdueWPsDisplay: React.FC = () => { const OverdueWorkPackagesView: React.FC = ({ workPackages }) => { const theme = useTheme(); + const isEmpty = workPackages.length === 0; return ( - + = ({ workP = ({ workP > - - {workPackages.length === 0 ? : workPackages.map((wp) => )} + + {isEmpty ? : workPackages.map((wp) => )} From b71ec0050d967128ef0679fe80c0667d550414a2 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 16:08:51 -0500 Subject: [PATCH 20/32] #2821-yarn install --- .../src/pages/HomePage/components/WorkPackagesSelectionView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx b/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx index 2b05907c4d..abbcaee8b6 100644 --- a/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx +++ b/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx @@ -53,7 +53,7 @@ const WorkPackagesSelectionView: React.FC = () => { const [currentDisplayedWPs, setCurrentDisplayedWPs] = useState(defaultFirstDisplay); - // destructuring tuple to get wps of selected option + // destructuring tuple to get wps of selected options const [, currentWps] = workPackageOptions[currentDisplayedWPs]; const WorkPackagesDisplay = (workPackages: WorkPackage[]) => ( From b3718e01a5544d679ca2cabc6319e06a16a70f49 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Sun, 8 Dec 2024 16:11:51 -0500 Subject: [PATCH 21/32] #2821-up to date --- .../src/pages/HomePage/components/WorkPackagesSelectionView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx b/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx index abbcaee8b6..2b05907c4d 100644 --- a/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx +++ b/src/frontend/src/pages/HomePage/components/WorkPackagesSelectionView.tsx @@ -53,7 +53,7 @@ const WorkPackagesSelectionView: React.FC = () => { const [currentDisplayedWPs, setCurrentDisplayedWPs] = useState(defaultFirstDisplay); - // destructuring tuple to get wps of selected options + // destructuring tuple to get wps of selected option const [, currentWps] = workPackageOptions[currentDisplayedWPs]; const WorkPackagesDisplay = (workPackages: WorkPackage[]) => ( From 94d8342252d90596250c12ced2bb9db9e6386d33 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 10 Dec 2024 20:28:05 -0500 Subject: [PATCH 22/32] #2997-reset to service --- .../src/controllers/users.controllers.ts | 12 ----- .../notifications.query-args.ts | 11 ++++ .../migration.sql | 51 +++++++++++++++++++ src/backend/src/routes/users.routes.ts | 6 --- src/backend/src/services/users.services.ts | 23 --------- .../transformers/notification.transformer.ts | 13 +++++ .../src/utils/homepage-notifications.utils.ts | 37 ++++++++++++++ .../tests/unmocked/notifications.test.ts | 49 ++++++++++++++++++ src/backend/tests/unmocked/users.test.ts | 24 --------- src/shared/index.ts | 1 + src/shared/src/types/notification.types.ts | 5 ++ 11 files changed, 167 insertions(+), 65 deletions(-) create mode 100644 src/backend/src/prisma-query-args/notifications.query-args.ts create mode 100644 src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql create mode 100644 src/backend/src/transformers/notification.transformer.ts create mode 100644 src/backend/src/utils/homepage-notifications.utils.ts create mode 100644 src/backend/tests/unmocked/notifications.test.ts create mode 100644 src/shared/src/types/notification.types.ts diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index efac029e88..5cfa740368 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -191,16 +191,4 @@ export default class UsersController { return next(error); } } - - static async sendNotitifcation(req: Request, res: Response, next: NextFunction) { - try { - const { userId } = req.params; - const { text, iconName } = req.body; - - const updatedUser = await UsersService.sendNotification(userId, text, iconName); - return res.status(200).json(updatedUser); - } catch (error: unknown) { - return 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 new file mode 100644 index 0000000000..9081f717cc --- /dev/null +++ b/src/backend/src/prisma-query-args/notifications.query-args.ts @@ -0,0 +1,11 @@ +import { Prisma } from '@prisma/client'; +import { getUserQueryArgs } from './user.query-args'; + +export type NotificationQueryArgs = ReturnType; + +export const getNotificationQueryArgs = (organizationId: string) => + Prisma.validator()({ + include: { + usersReceived: getUserQueryArgs(organizationId) + } + }); diff --git a/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql b/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql new file mode 100644 index 0000000000..192243b385 --- /dev/null +++ b/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql @@ -0,0 +1,51 @@ +/* + Warnings: + + - You are about to drop the column `userId` on the `Notification` table. All the data in the column will be lost. + - Added the required column `text` to the `Announcement` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Notification" DROP CONSTRAINT "Notification_userId_fkey"; + +-- AlterTable +ALTER TABLE "Announcement" ADD COLUMN "text" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Notification" DROP COLUMN "userId"; + +-- CreateTable +CREATE TABLE "_ReceivedAnnouncements" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_NotificationToUser" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_NotificationToUser_AB_unique" ON "_NotificationToUser"("A", "B"); + +-- CreateIndex +CREATE INDEX "_NotificationToUser_B_index" ON "_NotificationToUser"("B"); + +-- AddForeignKey +ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 99f7802b40..2f95201f6f 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,11 +54,5 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); -userRouter.post( - `/:userId/notifications/send`, - nonEmptyString(body('text')), - nonEmptyString(body('iconName')), - UsersController.sendNotitifcation -); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index c617a642b1..4c7d2b4007 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -571,27 +571,4 @@ export default class UsersService { const resolvedTasks = await Promise.all(tasksPromises); return resolvedTasks.flat(); } - - static async sendNotification(userId: string, text: string, iconName: string) { - const requestedUser = await prisma.user.findUnique({ - where: { userId } - }); - - if (!requestedUser) throw new NotFoundException('User', userId); - - const createdNotification = await prisma.notification.create({ - data: { - text, - iconName - } - }); - - const udaptedUser = await prisma.user.update({ - where: { userId: requestedUser.userId }, - data: { unreadNotifications: { connect: createdNotification } }, - include: { unreadNotifications: true } - }); - - return udaptedUser; - } } diff --git a/src/backend/src/transformers/notification.transformer.ts b/src/backend/src/transformers/notification.transformer.ts new file mode 100644 index 0000000000..32666b151a --- /dev/null +++ b/src/backend/src/transformers/notification.transformer.ts @@ -0,0 +1,13 @@ +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 + }; +}; + +export default notificationTransformer; diff --git a/src/backend/src/utils/homepage-notifications.utils.ts b/src/backend/src/utils/homepage-notifications.utils.ts new file mode 100644 index 0000000000..f795064c28 --- /dev/null +++ b/src/backend/src/utils/homepage-notifications.utils.ts @@ -0,0 +1,37 @@ +import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; +import prisma from '../prisma/prisma'; +import notificationTransformer from '../transformers/notification.transformer'; +import { NotFoundException } from './errors.utils'; + +const sendNotificationToUser = async (userId: string, notificationId: string, organizationId: string) => { + const requestedUser = await prisma.user.findUnique({ + where: { userId } + }); + + if (!requestedUser) throw new NotFoundException('User', userId); + + const updatedUser = await prisma.user.update({ + where: { userId: requestedUser.userId }, + data: { unreadNotifications: { connect: { notificationId } } }, + include: { unreadNotifications: getNotificationQueryArgs(organizationId) } + }); + + return updatedUser.unreadNotifications.map(notificationTransformer); +}; + +export const sendNotificationToUsers = async (userIds: string[], text: string, iconName: string, organizationId: string) => { + const createdNotification = await prisma.notification.create({ + data: { + text, + iconName + }, + ...getNotificationQueryArgs(organizationId) + }); + + const notificationPromises = userIds.map(async (userId) => { + return sendNotificationToUser(userId, createdNotification.notificationId, organizationId); + }); + + await Promise.all(notificationPromises); + return notificationTransformer(createdNotification); +}; diff --git a/src/backend/tests/unmocked/notifications.test.ts b/src/backend/tests/unmocked/notifications.test.ts new file mode 100644 index 0000000000..eea07b47e4 --- /dev/null +++ b/src/backend/tests/unmocked/notifications.test.ts @@ -0,0 +1,49 @@ +import { Organization } from '@prisma/client'; +import { createTestOrganization, createTestUser, resetUsers } from '../test-utils'; +import { batmanAppAdmin, wonderwomanGuest } from '../test-data/users.test-data'; +import { NotFoundException } from '../../src/utils/errors.utils'; +import { sendNotificationToUsers } from '../../src/utils/homepage-notifications.utils'; +import prisma from '../../src/prisma/prisma'; + +describe('Notification 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 sendNotificationToUsers(['1'], 'test', 'test', orgId)).rejects.toThrow( + new NotFoundException('User', '1') + ); + }); + + it('Succeeds and sends notification to user', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + const testWonderWoman = await createTestUser(wonderwomanGuest, orgId); + + const notification = await sendNotificationToUsers([testBatman.userId, testWonderWoman.userId], 'test', 'icon', orgId); + + const batmanWithNotifications = await prisma.user.findUnique({ + where: { userId: testBatman.userId }, + include: { unreadNotifications: true } + }); + const wonderWomanWithNotifications = await prisma.user.findUnique({ + where: { userId: testWonderWoman.userId }, + include: { unreadNotifications: true } + }); + + expect(batmanWithNotifications?.unreadNotifications).toHaveLength(1); + expect(batmanWithNotifications?.unreadNotifications[0]).toStrictEqual(notification); + + expect(wonderWomanWithNotifications?.unreadNotifications).toHaveLength(1); + expect(wonderWomanWithNotifications?.unreadNotifications[0]).toStrictEqual(notification); + }); + }); +}); diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index b26a941bd5..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 prisma from '../../src/prisma/prisma'; describe('User Tests', () => { let orgId: string; @@ -49,27 +48,4 @@ describe('User Tests', () => { expect(userTasks).toStrictEqual([batmanTask, batmanTask]); }); }); - - describe('Send Notification', () => { - it('fails on invalid user id', async () => { - await expect(async () => await UsersService.sendNotification('1', 'test', 'test')).rejects.toThrow( - new NotFoundException('User', '1') - ); - }); - - it('Succeeds and sends notification to user', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await UsersService.sendNotification(testBatman.userId, 'test1', 'test1'); - await UsersService.sendNotification(testBatman.userId, 'test2', 'test2'); - - const batmanWithNotifications = await prisma.user.findUnique({ - where: { userId: testBatman.userId }, - include: { unreadNotifications: true } - }); - - expect(batmanWithNotifications?.unreadNotifications).toHaveLength(2); - expect(batmanWithNotifications?.unreadNotifications[0].text).toBe('test1'); - expect(batmanWithNotifications?.unreadNotifications[1].text).toBe('test2'); - }); - }); }); diff --git a/src/shared/index.ts b/src/shared/index.ts index 1d3f6c399f..763ce5e09e 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -11,6 +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/notification.types'; export * from './src/validate-wbs'; export * from './src/date-utils'; diff --git a/src/shared/src/types/notification.types.ts b/src/shared/src/types/notification.types.ts new file mode 100644 index 0000000000..e4419ef2ed --- /dev/null +++ b/src/shared/src/types/notification.types.ts @@ -0,0 +1,5 @@ +export interface Notification { + notificationId: string; + text: string; + iconName: string; +} From edb600cf9c7d2872178b4158728a2cfb4eb507f7 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Tue, 10 Dec 2024 21:20:36 -0500 Subject: [PATCH 23/32] #2997-added endpoints for notifications --- .../src/controllers/users.controllers.ts | 24 +++++++++-- .../notifications.query-args.ts | 11 +++++ src/backend/src/routes/users.routes.ts | 9 +++- src/backend/src/services/users.services.ts | 43 ++++++++++++++++--- .../transformers/notifications.transformer.ts | 13 ++++++ src/shared/index.ts | 2 +- src/shared/src/types/notifications.types.ts | 5 +++ 7 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 src/backend/src/prisma-query-args/notifications.query-args.ts create mode 100644 src/backend/src/transformers/notifications.transformer.ts create mode 100644 src/shared/src/types/notifications.types.ts diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index fbd410d752..115737daf1 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -195,12 +195,28 @@ export default class UsersController { static async sendNotitifcation(req: Request, res: Response, next: NextFunction) { try { const { userId } = req.params; - const { text, iconName } = req.body; + const { notificationId } = req.body; + const { organization } = req; - const updatedUser = await UsersService.sendNotification(userId, text, iconName); - return res.status(200).json(updatedUser); + const updatedUser = await UsersService.sendNotification(userId, notificationId, organization); + res.status(200).json(updatedUser); } catch (error: unknown) { - return next(error); + next(error); + } + } + + static async sendNotificationToManyUsers(req: Request, res: Response, next: NextFunction) { + try { + console.log('CONTROLLER'); + const { text, iconName, userIds } = req.body; + console.log(text); + console.log(iconName); + console.log(userIds); + + const createdNotification = await UsersService.sendNotifcationToManyUsers(userIds, text, iconName, req.organization); + res.status(200).json(createdNotification); + } 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 new file mode 100644 index 0000000000..4cf877ac5c --- /dev/null +++ b/src/backend/src/prisma-query-args/notifications.query-args.ts @@ -0,0 +1,11 @@ +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/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 99f7802b40..a4228e0e9b 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,11 +54,16 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); +userRouter.post(`/:userId/notifications/send`, nonEmptyString(body('notificationId')), UsersController.sendNotitifcation); + userRouter.post( - `/:userId/notifications/send`, + 'notifications/send/many', nonEmptyString(body('text')), nonEmptyString(body('iconName')), - UsersController.sendNotitifcation + body('userIds').isArray(), + nonEmptyString(body('userIds.*')), + validateInputs, + UsersController.sendNotificationToManyUsers ); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index a5fecd4867..1fcfa8cf4a 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -38,6 +38,8 @@ 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 notificationTransformer from '../transformers/notifications.transformer'; +import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; export default class UsersService { /** @@ -567,26 +569,53 @@ export default class UsersService { return resolvedTasks.flat(); } - static async sendNotification(userId: string, text: string, iconName: string) { + /** + * Sends a notification to a user + * @param userId + * @param notificationId + * @param organization + * @returns the updated unread notification of the user + */ + static async sendNotification(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: requestedUser.userId }, + data: { unreadNotifications: { connect: { notificationId } } }, + include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } + }); + + 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 organization + * @returns the created notification + */ + static async sendNotifcationToManyUsers(text: string, iconName: string, userIds: string[], organization: Organization) { const createdNotification = await prisma.notification.create({ data: { text, iconName - } + }, + ...getNotificationQueryArgs(organization.organizationId) }); - const udaptedUser = await prisma.user.update({ - where: { userId: requestedUser.userId }, - data: { unreadNotifications: { connect: createdNotification } }, - include: { unreadNotifications: true } + if (!createdNotification) throw new HttpException(500, 'Failed to create notification'); + + const notificationsPromises = userIds.map(async (userId) => { + return UsersService.sendNotification(userId, createdNotification.notificationId, organization); }); - return udaptedUser; + await Promise.all(notificationsPromises); + return notificationTransformer(createdNotification); } } diff --git a/src/backend/src/transformers/notifications.transformer.ts b/src/backend/src/transformers/notifications.transformer.ts new file mode 100644 index 0000000000..32666b151a --- /dev/null +++ b/src/backend/src/transformers/notifications.transformer.ts @@ -0,0 +1,13 @@ +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 + }; +}; + +export default notificationTransformer; diff --git a/src/shared/index.ts b/src/shared/index.ts index 1d3f6c399f..409dae2e65 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/validate-wbs'; export * from './src/date-utils'; diff --git a/src/shared/src/types/notifications.types.ts b/src/shared/src/types/notifications.types.ts new file mode 100644 index 0000000000..e4419ef2ed --- /dev/null +++ b/src/shared/src/types/notifications.types.ts @@ -0,0 +1,5 @@ +export interface Notification { + notificationId: string; + text: string; + iconName: string; +} From 1650ab83e5c058118920eb5a7fc97e33ba69dd67 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 13:59:01 -0500 Subject: [PATCH 24/32] #2997-send notifications endpoint tested --- .../src/controllers/users.controllers.ts | 19 ++----- .../migration.sql | 24 -------- .../migration.sql | 57 +++++++++++++++++++ src/backend/src/prisma/schema.prisma | 4 +- src/backend/src/routes/users.routes.ts | 5 +- src/backend/src/services/users.services.ts | 34 +++-------- 6 files changed, 74 insertions(+), 69 deletions(-) delete mode 100644 src/backend/src/prisma/migrations/20241112180715_announcements_and_notifications/migration.sql create mode 100644 src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index 115737daf1..1fa6c9fde7 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -174,6 +174,8 @@ export default class UsersController { const { userId } = req.params; const { organization } = req; + console.log('TASK'); + const userTasks = await UsersService.getUserTasks(userId, organization); res.status(200).json(userTasks); } catch (error: unknown) { @@ -192,20 +194,7 @@ export default class UsersController { } } - static async sendNotitifcation(req: Request, res: Response, next: NextFunction) { - try { - const { userId } = req.params; - const { notificationId } = req.body; - const { organization } = req; - - const updatedUser = await UsersService.sendNotification(userId, notificationId, organization); - res.status(200).json(updatedUser); - } catch (error: unknown) { - next(error); - } - } - - static async sendNotificationToManyUsers(req: Request, res: Response, next: NextFunction) { + static async sendNotificationToUsers(req: Request, res: Response, next: NextFunction) { try { console.log('CONTROLLER'); const { text, iconName, userIds } = req.body; @@ -213,7 +202,7 @@ export default class UsersController { console.log(iconName); console.log(userIds); - const createdNotification = await UsersService.sendNotifcationToManyUsers(userIds, text, iconName, req.organization); + const createdNotification = await UsersService.sendNotifcationToUsers(text, iconName, userIds, req.organization); res.status(200).json(createdNotification); } catch (error: unknown) { next(error); diff --git a/src/backend/src/prisma/migrations/20241112180715_announcements_and_notifications/migration.sql b/src/backend/src/prisma/migrations/20241112180715_announcements_and_notifications/migration.sql deleted file mode 100644 index 91b483cea3..0000000000 --- a/src/backend/src/prisma/migrations/20241112180715_announcements_and_notifications/migration.sql +++ /dev/null @@ -1,24 +0,0 @@ --- CreateTable -CREATE TABLE "Announcement" ( - "announcementId" TEXT NOT NULL, - "userCreatedId" TEXT NOT NULL, - "dateCrated" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Announcement_pkey" PRIMARY KEY ("announcementId") -); - --- CreateTable -CREATE TABLE "Notification" ( - "notificationId" TEXT NOT NULL, - "text" TEXT NOT NULL, - "iconName" TEXT NOT NULL, - "userId" TEXT, - - CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId") -); - --- AddForeignKey -ALTER TABLE "Announcement" ADD CONSTRAINT "Announcement_userCreatedId_fkey" FOREIGN KEY ("userCreatedId") REFERENCES "User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql b/src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql new file mode 100644 index 0000000000..30b887407d --- /dev/null +++ b/src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql @@ -0,0 +1,57 @@ +-- CreateTable +CREATE TABLE "Announcement" ( + "announcementId" TEXT NOT NULL, + "text" TEXT NOT NULL, + "dateCrated" TIMESTAMP(3) NOT NULL, + "userCreatedId" TEXT NOT NULL, + + CONSTRAINT "Announcement_pkey" PRIMARY KEY ("announcementId") +); + +-- CreateTable +CREATE TABLE "Notification" ( + "notificationId" TEXT NOT NULL, + "text" TEXT NOT NULL, + "iconName" TEXT NOT NULL, + + CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId") +); + +-- CreateTable +CREATE TABLE "_ReceivedAnnouncements" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_UserNotifications" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_UserNotifications_AB_unique" ON "_UserNotifications"("A", "B"); + +-- CreateIndex +CREATE INDEX "_UserNotifications_B_index" ON "_UserNotifications"("B"); + +-- 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; + +-- AddForeignKey +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; + +-- AddForeignKey +ALTER TABLE "_UserNotifications" ADD CONSTRAINT "_UserNotifications_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 7023bbea12..0f010392ea 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -182,7 +182,7 @@ model User { deletedMilestones Milestone[] @relation(name: "milestoneDeleter") receivedAnnouncements Announcement[] @relation(name: "ReceivedAnnouncements") createdAnnouncements Announcement[] @relation(name: "CreatedAnnouncements") - unreadNotifications Notification[] + unreadNotifications Notification[] @relation(name: "UserNotifications") } model Role { @@ -944,5 +944,5 @@ model Notification { notificationId String @id @default(uuid()) text String iconName String - usersReceived User[] + users User[] @relation("UserNotifications") } diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index a4228e0e9b..4d7fca68f2 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -54,16 +54,15 @@ userRouter.post( validateInputs, UsersController.getManyUserTasks ); -userRouter.post(`/:userId/notifications/send`, nonEmptyString(body('notificationId')), UsersController.sendNotitifcation); userRouter.post( - 'notifications/send/many', + '/notifications/send/many', nonEmptyString(body('text')), nonEmptyString(body('iconName')), body('userIds').isArray(), nonEmptyString(body('userIds.*')), validateInputs, - UsersController.sendNotificationToManyUsers + UsersController.sendNotificationToUsers ); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 1fcfa8cf4a..3bd1e99812 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -569,29 +569,6 @@ export default class UsersService { return resolvedTasks.flat(); } - /** - * Sends a notification to a user - * @param userId - * @param notificationId - * @param organization - * @returns the updated unread notification of the user - */ - static async sendNotification(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: requestedUser.userId }, - data: { unreadNotifications: { connect: { notificationId } } }, - include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) } - }); - - return updatedUser.unreadNotifications.map(notificationTransformer); - } - /** * Creates and sends a notification to all users with the given userIds * @param text writing in the notification @@ -600,7 +577,7 @@ export default class UsersService { * @param organization * @returns the created notification */ - static async sendNotifcationToManyUsers(text: string, iconName: string, userIds: string[], organization: Organization) { + static async sendNotifcationToUsers(text: string, iconName: string, userIds: string[], organization: Organization) { const createdNotification = await prisma.notification.create({ data: { text, @@ -612,7 +589,14 @@ export default class UsersService { if (!createdNotification) throw new HttpException(500, 'Failed to create notification'); const notificationsPromises = userIds.map(async (userId) => { - return UsersService.sendNotification(userId, createdNotification.notificationId, organization); + return await prisma.user.update({ + where: { userId }, + data: { + unreadNotifications: { + connect: { notificationId: createdNotification.notificationId } + } + } + }); }); await Promise.all(notificationsPromises); From abe2400c050a5501ef02a1de18ecf9852ed36fb4 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 14:06:16 -0500 Subject: [PATCH 25/32] #2997-created tests for notifications --- src/backend/src/services/users.services.ts | 8 +++++- src/backend/tests/unmocked/users.test.ts | 29 +++++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index 3bd1e99812..b680811d1b 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -589,8 +589,14 @@ export default class UsersService { 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 }, + where: { userId: requestedUser.userId }, data: { unreadNotifications: { connect: { notificationId: createdNotification.notificationId } diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index b26a941bd5..4d59d49dd1 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -1,6 +1,6 @@ import { Organization } from '@prisma/client'; import { createTestOrganization, createTestTask, createTestUser, resetUsers } from '../test-utils'; -import { batmanAppAdmin } from '../test-data/users.test-data'; +import { batmanAppAdmin, supermanAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; import prisma from '../../src/prisma/prisma'; @@ -52,24 +52,35 @@ describe('User Tests', () => { describe('Send Notification', () => { it('fails on invalid user id', async () => { - await expect(async () => await UsersService.sendNotification('1', 'test', 'test')).rejects.toThrow( - new NotFoundException('User', '1') - ); + await expect( + async () => await UsersService.sendNotifcationToUsers('test notification', 'star', ['1', '2'], organization) + ).rejects.toThrow(new NotFoundException('User', '1')); }); it('Succeeds and sends notification to user', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); - await UsersService.sendNotification(testBatman.userId, 'test1', 'test1'); - await UsersService.sendNotification(testBatman.userId, 'test2', 'test2'); + const testSuperman = await createTestUser(supermanAdmin, orgId); + await UsersService.sendNotifcationToUsers( + 'test notification', + 'star', + [testBatman.userId, testSuperman.userId], + organization + ); const batmanWithNotifications = await prisma.user.findUnique({ where: { userId: testBatman.userId }, include: { unreadNotifications: true } }); - expect(batmanWithNotifications?.unreadNotifications).toHaveLength(2); - expect(batmanWithNotifications?.unreadNotifications[0].text).toBe('test1'); - expect(batmanWithNotifications?.unreadNotifications[1].text).toBe('test2'); + 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'); }); }); }); From 22002563540f521bd39d8d3b062b71ed82f69dc5 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 14:29:09 -0500 Subject: [PATCH 26/32] #2997-small issues --- .../src/controllers/users.controllers.ts | 9 ---- .../notifications.query-args.ts | 4 -- .../migration.sql | 51 ------------------- src/backend/tests/unmocked/users.test.ts | 1 + 4 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index 00b9da97a1..1411e440a8 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -174,8 +174,6 @@ export default class UsersController { const { userId } = req.params; const { organization } = req; - console.log('TASK'); - const userTasks = await UsersService.getUserTasks(userId, organization); res.status(200).json(userTasks); } catch (error: unknown) { @@ -193,16 +191,10 @@ export default class UsersController { next(error); } } -<<<<<<< HEAD -======= static async sendNotificationToUsers(req: Request, res: Response, next: NextFunction) { try { - console.log('CONTROLLER'); const { text, iconName, userIds } = req.body; - console.log(text); - console.log(iconName); - console.log(userIds); const createdNotification = await UsersService.sendNotifcationToUsers(text, iconName, userIds, req.organization); res.status(200).json(createdNotification); @@ -210,5 +202,4 @@ export default class UsersController { next(error); } } ->>>>>>> Send-Notification-Update } diff --git a/src/backend/src/prisma-query-args/notifications.query-args.ts b/src/backend/src/prisma-query-args/notifications.query-args.ts index af5debecd6..4cf877ac5c 100644 --- a/src/backend/src/prisma-query-args/notifications.query-args.ts +++ b/src/backend/src/prisma-query-args/notifications.query-args.ts @@ -6,10 +6,6 @@ export type NotificationQueryArgs = ReturnType; export const getNotificationQueryArgs = (organizationId: string) => Prisma.validator()({ include: { -<<<<<<< HEAD - usersReceived: getUserQueryArgs(organizationId) -======= users: getUserQueryArgs(organizationId) ->>>>>>> Send-Notification-Update } }); diff --git a/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql b/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql deleted file mode 100644 index 192243b385..0000000000 --- a/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql +++ /dev/null @@ -1,51 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `userId` on the `Notification` table. All the data in the column will be lost. - - Added the required column `text` to the `Announcement` table without a default value. This is not possible if the table is not empty. - -*/ --- DropForeignKey -ALTER TABLE "Notification" DROP CONSTRAINT "Notification_userId_fkey"; - --- AlterTable -ALTER TABLE "Announcement" ADD COLUMN "text" TEXT NOT NULL; - --- AlterTable -ALTER TABLE "Notification" DROP COLUMN "userId"; - --- CreateTable -CREATE TABLE "_ReceivedAnnouncements" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateTable -CREATE TABLE "_NotificationToUser" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); - --- CreateIndex -CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); - --- CreateIndex -CREATE UNIQUE INDEX "_NotificationToUser_AB_unique" ON "_NotificationToUser"("A", "B"); - --- CreateIndex -CREATE INDEX "_NotificationToUser_B_index" ON "_NotificationToUser"("B"); - --- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 5827a8507e..4d59d49dd1 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -3,6 +3,7 @@ import { createTestOrganization, createTestTask, createTestUser, resetUsers } fr import { batmanAppAdmin, supermanAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; +import prisma from '../../src/prisma/prisma'; describe('User Tests', () => { let orgId: string; From 151ce732b5974149e884df3d6c58820aab1a25f8 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 14:32:37 -0500 Subject: [PATCH 27/32] #2997-removed duplicates --- .../transformers/notification.transformer.ts | 13 ----- .../tests/unmocked/notifications.test.ts | 49 ------------------- src/shared/src/types/notification.types.ts | 5 -- 3 files changed, 67 deletions(-) delete mode 100644 src/backend/src/transformers/notification.transformer.ts delete mode 100644 src/backend/tests/unmocked/notifications.test.ts delete mode 100644 src/shared/src/types/notification.types.ts diff --git a/src/backend/src/transformers/notification.transformer.ts b/src/backend/src/transformers/notification.transformer.ts deleted file mode 100644 index 32666b151a..0000000000 --- a/src/backend/src/transformers/notification.transformer.ts +++ /dev/null @@ -1,13 +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 - }; -}; - -export default notificationTransformer; diff --git a/src/backend/tests/unmocked/notifications.test.ts b/src/backend/tests/unmocked/notifications.test.ts deleted file mode 100644 index eea07b47e4..0000000000 --- a/src/backend/tests/unmocked/notifications.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Organization } from '@prisma/client'; -import { createTestOrganization, createTestUser, resetUsers } from '../test-utils'; -import { batmanAppAdmin, wonderwomanGuest } from '../test-data/users.test-data'; -import { NotFoundException } from '../../src/utils/errors.utils'; -import { sendNotificationToUsers } from '../../src/utils/homepage-notifications.utils'; -import prisma from '../../src/prisma/prisma'; - -describe('Notification 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 sendNotificationToUsers(['1'], 'test', 'test', orgId)).rejects.toThrow( - new NotFoundException('User', '1') - ); - }); - - it('Succeeds and sends notification to user', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - const testWonderWoman = await createTestUser(wonderwomanGuest, orgId); - - const notification = await sendNotificationToUsers([testBatman.userId, testWonderWoman.userId], 'test', 'icon', orgId); - - const batmanWithNotifications = await prisma.user.findUnique({ - where: { userId: testBatman.userId }, - include: { unreadNotifications: true } - }); - const wonderWomanWithNotifications = await prisma.user.findUnique({ - where: { userId: testWonderWoman.userId }, - include: { unreadNotifications: true } - }); - - expect(batmanWithNotifications?.unreadNotifications).toHaveLength(1); - expect(batmanWithNotifications?.unreadNotifications[0]).toStrictEqual(notification); - - expect(wonderWomanWithNotifications?.unreadNotifications).toHaveLength(1); - expect(wonderWomanWithNotifications?.unreadNotifications[0]).toStrictEqual(notification); - }); - }); -}); diff --git a/src/shared/src/types/notification.types.ts b/src/shared/src/types/notification.types.ts deleted file mode 100644 index e4419ef2ed..0000000000 --- a/src/shared/src/types/notification.types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Notification { - notificationId: string; - text: string; - iconName: string; -} From e7df823f4e19bce9df5c8df2cae3b4ca5816e3d7 Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 14:37:39 -0500 Subject: [PATCH 28/32] #2997-removed utils --- .../src/utils/homepage-notifications.utils.ts | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/backend/src/utils/homepage-notifications.utils.ts diff --git a/src/backend/src/utils/homepage-notifications.utils.ts b/src/backend/src/utils/homepage-notifications.utils.ts deleted file mode 100644 index f795064c28..0000000000 --- a/src/backend/src/utils/homepage-notifications.utils.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; -import prisma from '../prisma/prisma'; -import notificationTransformer from '../transformers/notification.transformer'; -import { NotFoundException } from './errors.utils'; - -const sendNotificationToUser = async (userId: string, notificationId: string, organizationId: string) => { - const requestedUser = await prisma.user.findUnique({ - where: { userId } - }); - - if (!requestedUser) throw new NotFoundException('User', userId); - - const updatedUser = await prisma.user.update({ - where: { userId: requestedUser.userId }, - data: { unreadNotifications: { connect: { notificationId } } }, - include: { unreadNotifications: getNotificationQueryArgs(organizationId) } - }); - - return updatedUser.unreadNotifications.map(notificationTransformer); -}; - -export const sendNotificationToUsers = async (userIds: string[], text: string, iconName: string, organizationId: string) => { - const createdNotification = await prisma.notification.create({ - data: { - text, - iconName - }, - ...getNotificationQueryArgs(organizationId) - }); - - const notificationPromises = userIds.map(async (userId) => { - return sendNotificationToUser(userId, createdNotification.notificationId, organizationId); - }); - - await Promise.all(notificationPromises); - return notificationTransformer(createdNotification); -}; From 2d011f4a5cdef9100aab2b5813624954e0f0980a Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 14:44:26 -0500 Subject: [PATCH 29/32] #2769-added loading check --- .../AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx index cda841a38d..f3fd972364 100644 --- a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx @@ -10,7 +10,7 @@ import { projectWbsNamePipe } from '../../../utils/pipes'; const EditFeaturedProjects = () => { const { data: featuredProjects, isLoading, isError, error } = useFeaturedProjects(); - const { mutateAsync: setFeaturedProjects } = useSetFeaturedProjects(); + const { mutateAsync: setFeaturedProjects, isLoading: setFeaturedProjectsIsLoading } = useSetFeaturedProjects(); const [isEditMode, setIsEditMode] = useState(false); const theme = useTheme(); const toast = useToast(); @@ -31,7 +31,7 @@ const EditFeaturedProjects = () => { handleClose(); }; - if (isLoading || !featuredProjects) return ; + if (isLoading || !featuredProjects || setFeaturedProjectsIsLoading || !setFeaturedProjects) return ; if (isError) return ; return ( From 91b54298f1ca8f4ddf8389c3af67880d3838d29b Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 15:15:54 -0500 Subject: [PATCH 30/32] #2997-include only notification service --- .../src/controllers/users.controllers.ts | 11 ---- .../migration.sql | 20 +++---- src/backend/src/prisma/schema.prisma | 12 ++-- .../src/routes/notifications.routes.ts | 11 ++++ src/backend/src/routes/users.routes.ts | 10 ---- .../src/services/notifications.services.ts | 44 +++++++++++++- src/backend/src/services/users.services.ts | 42 ------------- .../tests/unmocked/notifications.test.ts | 59 +++++++++++++++++++ src/backend/tests/unmocked/users.test.ts | 37 +----------- 9 files changed, 130 insertions(+), 116 deletions(-) rename src/backend/src/prisma/migrations/{20241211185407_announcements_and_notifications => 20241211195435_announcements_and_notifications}/migration.sql (70%) create mode 100644 src/backend/tests/unmocked/notifications.test.ts diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index 1411e440a8..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 sendNotificationToUsers(req: Request, res: Response, next: NextFunction) { - try { - const { text, iconName, userIds } = req.body; - - const createdNotification = await UsersService.sendNotifcationToUsers(text, iconName, userIds, req.organization); - res.status(200).json(createdNotification); - } catch (error: unknown) { - next(error); - } - } } diff --git a/src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql b/src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql similarity index 70% rename from src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql rename to src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql index 30b887407d..c57afabd25 100644 --- a/src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql +++ b/src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql @@ -18,40 +18,40 @@ CREATE TABLE "Notification" ( ); -- CreateTable -CREATE TABLE "_ReceivedAnnouncements" ( +CREATE TABLE "_receivedAnnouncements" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); -- CreateTable -CREATE TABLE "_UserNotifications" ( +CREATE TABLE "_userNotifications" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); -- CreateIndex -CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); +CREATE UNIQUE INDEX "_receivedAnnouncements_AB_unique" ON "_receivedAnnouncements"("A", "B"); -- CreateIndex -CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); +CREATE INDEX "_receivedAnnouncements_B_index" ON "_receivedAnnouncements"("B"); -- CreateIndex -CREATE UNIQUE INDEX "_UserNotifications_AB_unique" ON "_UserNotifications"("A", "B"); +CREATE UNIQUE INDEX "_userNotifications_AB_unique" ON "_userNotifications"("A", "B"); -- CreateIndex -CREATE INDEX "_UserNotifications_B_index" ON "_UserNotifications"("B"); +CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B"); -- 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; +ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; +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 "_userNotifications" ADD CONSTRAINT "_userNotifications_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") 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 "_userNotifications" ADD CONSTRAINT "_userNotifications_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 0f010392ea..c06ca3d45f 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -180,9 +180,9 @@ model User { deletedFrequentlyAskedQuestions FrequentlyAskedQuestion[] @relation(name: "frequentlyAskedQuestionDeleter") createdMilestones Milestone[] @relation(name: "milestoneCreator") deletedMilestones Milestone[] @relation(name: "milestoneDeleter") - receivedAnnouncements Announcement[] @relation(name: "ReceivedAnnouncements") - createdAnnouncements Announcement[] @relation(name: "CreatedAnnouncements") - unreadNotifications Notification[] @relation(name: "UserNotifications") + receivedAnnouncements Announcement[] @relation(name: "receivedAnnouncements") + createdAnnouncements Announcement[] @relation(name: "createdAnnouncements") + unreadNotifications Notification[] @relation(name: "userNotifications") } model Role { @@ -934,15 +934,15 @@ model Milestone { model Announcement { announcementId String @id @default(uuid()) text String - usersReceived User[] @relation("ReceivedAnnouncements") + usersReceived User[] @relation("receivedAnnouncements") dateCrated DateTime userCreatedId String - userCreated User @relation("CreatedAnnouncements", fields: [userCreatedId], references: [userId]) + userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId]) } model Notification { notificationId String @id @default(uuid()) text String iconName String - users User[] @relation("UserNotifications") + users User[] @relation("userNotifications") } diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index 4701b0f3ea..ff712d8b48 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -1,8 +1,19 @@ import express from 'express'; import NotificationsController from '../controllers/notifications.controllers'; +import { nonEmptyString, validateInputs } from '../utils/validation.utils'; +import { body } from 'express-validator'; const notificationsRouter = express.Router(); notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications); +notificationsRouter.post( + '/send/users', + nonEmptyString(body('text')), + nonEmptyString(body('iconName')), + body('userIds').isArray(), + nonEmptyString(body('userIds.*')), + validateInputs, + NotificationsController.sendNotificationToUsers +); export default notificationsRouter; diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 4d7fca68f2..2f95201f6f 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -55,14 +55,4 @@ userRouter.post( UsersController.getManyUserTasks ); -userRouter.post( - '/notifications/send/many', - nonEmptyString(body('text')), - nonEmptyString(body('iconName')), - body('userIds').isArray(), - nonEmptyString(body('userIds.*')), - validateInputs, - UsersController.sendNotificationToUsers -); - export default userRouter; diff --git a/src/backend/src/services/notifications.services.ts b/src/backend/src/services/notifications.services.ts index a443d93588..483e6ed9d6 100644 --- a/src/backend/src/services/notifications.services.ts +++ b/src/backend/src/services/notifications.services.ts @@ -11,8 +11,10 @@ import { daysBetween, startOfDay, wbsPipe } from 'shared'; import { buildDueString } from '../utils/slack.utils'; import WorkPackagesService from './work-packages.services'; import { addWeeksToDate } from 'shared'; -import { HttpException } from '../utils/errors.utils'; +import { HttpException, NotFoundException } 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() { @@ -193,4 +195,44 @@ export default class NotificationsService { await Promise.all(promises); } + + /** + * 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 + * @returns the created notification + */ + static async sendNotifcationToUsers(text: string, iconName: string, userIds: string[], organizationId: string) { + const createdNotification = await prisma.notification.create({ + data: { + text, + iconName + }, + ...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/users.services.ts b/src/backend/src/services/users.services.ts index b680811d1b..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 notificationTransformer from '../transformers/notifications.transformer'; -import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; export default class UsersService { /** @@ -568,44 +566,4 @@ export default class UsersService { const resolvedTasks = await Promise.all(tasksPromises); return resolvedTasks.flat(); } - - /** - * 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 organization - * @returns the created notification - */ - static async sendNotifcationToUsers(text: string, iconName: string, userIds: string[], organization: Organization) { - const createdNotification = await prisma.notification.create({ - data: { - text, - iconName - }, - ...getNotificationQueryArgs(organization.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/tests/unmocked/notifications.test.ts b/src/backend/tests/unmocked/notifications.test.ts new file mode 100644 index 0000000000..d3cce68361 --- /dev/null +++ b/src/backend/tests/unmocked/notifications.test.ts @@ -0,0 +1,59 @@ +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'); + }); + }); +}); diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 4d59d49dd1..c13a0c857f 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -1,9 +1,8 @@ import { Organization } from '@prisma/client'; import { createTestOrganization, createTestTask, createTestUser, resetUsers } from '../test-utils'; -import { batmanAppAdmin, supermanAdmin } from '../test-data/users.test-data'; +import { batmanAppAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; -import prisma from '../../src/prisma/prisma'; describe('User Tests', () => { let orgId: string; @@ -49,38 +48,4 @@ describe('User Tests', () => { expect(userTasks).toStrictEqual([batmanTask, batmanTask]); }); }); - - describe('Send Notification', () => { - it('fails on invalid user id', async () => { - await expect( - async () => await UsersService.sendNotifcationToUsers('test notification', 'star', ['1', '2'], organization) - ).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 UsersService.sendNotifcationToUsers( - 'test notification', - 'star', - [testBatman.userId, testSuperman.userId], - organization - ); - - 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'); - }); - }); }); From d53dc2ecfe9ccf3aa6de535d2a77a8db32a93d9c Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 15:18:15 -0500 Subject: [PATCH 31/32] #2997-removed route --- src/backend/src/routes/notifications.routes.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index ff712d8b48..4701b0f3ea 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -1,19 +1,8 @@ import express from 'express'; import NotificationsController from '../controllers/notifications.controllers'; -import { nonEmptyString, validateInputs } from '../utils/validation.utils'; -import { body } from 'express-validator'; const notificationsRouter = express.Router(); notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications); -notificationsRouter.post( - '/send/users', - nonEmptyString(body('text')), - nonEmptyString(body('iconName')), - body('userIds').isArray(), - nonEmptyString(body('userIds.*')), - validateInputs, - NotificationsController.sendNotificationToUsers -); export default notificationsRouter; From af59d26f3c01c38876023ecc89d16ceb75d48a4c Mon Sep 17 00:00:00 2001 From: caiodasilva2005 Date: Wed, 11 Dec 2024 15:28:57 -0500 Subject: [PATCH 32/32] #2769-fixed loading check --- src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx | 4 +--- .../AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx b/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx index 653f6f7169..b96aa4c93b 100644 --- a/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx +++ b/src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx @@ -94,9 +94,7 @@ const AdminToolsPage: React.FC = () => { ) : tabIndex === 3 ? ( ) : tabIndex === 4 ? ( - - - + ) : ( diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx index f3fd972364..ae0d634f2f 100644 --- a/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/EditFeaturedProjects.tsx @@ -15,6 +15,9 @@ const EditFeaturedProjects = () => { const theme = useTheme(); const toast = useToast(); + if (isLoading || !featuredProjects || setFeaturedProjectsIsLoading || !setFeaturedProjects) return ; + if (isError) return ; + const handleClose = () => { setIsEditMode(false); }; @@ -31,9 +34,6 @@ const EditFeaturedProjects = () => { handleClose(); }; - if (isLoading || !featuredProjects || setFeaturedProjectsIsLoading || !setFeaturedProjects) return ; - if (isError) return ; - return (