From 6c0e3eeac4d3fbc2e6a7d91db2cf89b64d025a4e Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 16 Jul 2025 08:45:16 -0700 Subject: [PATCH 01/10] sent received on date with agency responses --- react-app/src/components/projects/ProjectDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/react-app/src/components/projects/ProjectDialog.tsx b/react-app/src/components/projects/ProjectDialog.tsx index fb7ec2af9..d4522bc31 100644 --- a/react-app/src/components/projects/ProjectDialog.tsx +++ b/react-app/src/components/projects/ProjectDialog.tsx @@ -518,6 +518,7 @@ export const ProjectAgencyResponseDialog = (props: IProjectAgencyResponseDialog) Response: Number(AgencyResponseType[agc.Response]), Note: agc.Note, ProjectId: initialValues.Id, + ReceivedOn: agc.ReceivedOn, })), ).then(() => postSubmit()); }} From 6f6137f33fb4d0ed78761cfb6cbcb06940b17d6e Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 16 Jul 2025 09:03:59 -0700 Subject: [PATCH 02/10] don't queue watch notifications if agency disabled --- .../src/services/notifications/notificationServices.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/express-api/src/services/notifications/notificationServices.ts b/express-api/src/services/notifications/notificationServices.ts index 9b605dfec..12708256c 100644 --- a/express-api/src/services/notifications/notificationServices.ts +++ b/express-api/src/services/notifications/notificationServices.ts @@ -677,8 +677,9 @@ const generateProjectWatchNotifications = async ( const agency = await query.manager.findOne(Agency, { where: { Id: response.AgencyId }, }); - //No use in queueing an email for an agency with no email address or that doesn't want notifications - if (agency?.SendEmail && agency?.Email && agency?.Email.length) { + // No use in queueing an email for an agency with no email address or that doesn't want notifications + // Don't queue notifications for disabled agencies either. + if (agency?.SendEmail && agency?.Email && agency?.Email.length && !agency.IsDisabled) { const dateInERP = project.ApprovedOn; const daysSinceThisStatus = getDaysBetween(dateInERP, new Date()); const statusNotifs = await query.manager.find(ProjectStatusNotification, { From b83bec2ad673bd5f75f936ce6b4e0e636a9f6419 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 16 Jul 2025 10:23:30 -0700 Subject: [PATCH 03/10] prevent invalid agency response subscriptions --- .../projects/projectsController.ts | 14 ++++++- .../src/services/projects/projectsServices.ts | 37 +++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/express-api/src/controllers/projects/projectsController.ts b/express-api/src/controllers/projects/projectsController.ts index c29405681..7bc07a008 100644 --- a/express-api/src/controllers/projects/projectsController.ts +++ b/express-api/src/controllers/projects/projectsController.ts @@ -215,11 +215,21 @@ export const updateProjectAgencyResponses = async (req: Request, res: Response) return res.status(403).send('Projects only editable by Administrator role.'); } - const notificationsSent = await projectServices.updateProjectAgencyResponses( + const updateResults = await projectServices.updateProjectAgencyResponses( projectId, req.body.responses, req.pimsUser, ); - return res.status(200).send(notificationsSent); + // Let requestor know if any of the responses were not updated due to invalid changes. + // This can happen if they are trying to subscribe an agency that is disabled, has no email, or has opted out of notifications. + if (updateResults.invalidResponseChanges > 0) { + return res + .status(400) + .send( + `${updateResults.invalidResponseChanges} of the responses were not updated due to invalid changes.`, + ); + } + + return res.status(200).send(updateResults); }; diff --git a/express-api/src/services/projects/projectsServices.ts b/express-api/src/services/projects/projectsServices.ts index a6d627aaa..7ed33ff37 100644 --- a/express-api/src/services/projects/projectsServices.ts +++ b/express-api/src/services/projects/projectsServices.ts @@ -800,7 +800,7 @@ const updateProjectAgencyResponses = async ( id: number, updatedResponses: Partial[], user: User, -): Promise => { +): Promise<{ sentNotifications: NotificationQueue[]; invalidResponseChanges: number }> => { if (!(await projectRepo.exists({ where: { Id: id } }))) { throw new ErrorWithCode('Project matching this internal ID not found.', 404); } @@ -851,14 +851,39 @@ const updateProjectAgencyResponses = async ( }, { DeletedById: user.Id, DeletedOn: new Date() }, ); + // Are any of the agencies involved disabled, not wanting notifications, or missing an email? + // We can't allow them to subcribe. + const relevantAgencies = await AppDataSource.getRepository(Agency).find({ + where: { + Id: In(updatedResponses.map((r) => r.AgencyId)), + }, + }); + const validReponses = updatedResponses.filter((resp) => { + // Only worry about this if subscribing. + if (resp.Response === AgencyResponseType.Subscribe) { + const agency = relevantAgencies.find((a) => a.Id === resp.AgencyId); + // If agency is not found, skip it. + if (!agency) return false; + // If agency is disabled, skip it. + if (agency.IsDisabled) return false; + // If agency does not want notifications, skip it. + if (!agency.SendEmail) return false; + // If agency does not have an email, skip it. + if (!agency.Email || agency.Email.trim() === '') return false; + } + // Otherwise, keep the response. + return true; + }); + // Save the remaining updated responses as current await AppDataSource.getRepository(ProjectAgencyResponse).save( - updatedResponses.map( + validReponses.map( (resp) => ({ ...resp, ProjectId: id, CreatedById: user.Id, + UpdatedById: user.Id, DeletedById: null, DeletedOn: null, }) as ProjectAgencyResponse, @@ -867,7 +892,7 @@ const updateProjectAgencyResponses = async ( // Identify which incoming responses are different than original ones on the project // This counts deleted ones as unsubscribed - const changedResponses = await getAgencyResponseChanges(originalResponses, updatedResponses); + const changedResponses = await getAgencyResponseChanges(originalResponses, validReponses); // For each of these changed/new responses, queue for send or cancel the notification as needed // Notifications are cancelled in this function, but only ones to send are returned in the list const notifsToSend = await notificationServices.generateProjectWatchNotifications( @@ -876,9 +901,13 @@ const updateProjectAgencyResponses = async ( ); // Send new notifcations - return await Promise.all( + const sentNotifications = await Promise.all( notifsToSend.map((notif) => notificationServices.sendNotification(notif, user)), ); + return { + sentNotifications, + invalidResponseChanges: updatedResponses.length - validReponses.length, + }; }; /** From 37d97b4241fa641f3bd760591410f723feb82d71 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 16 Jul 2025 11:48:44 -0700 Subject: [PATCH 04/10] agency response table formatting --- .../src/components/projects/AgencyResponseSearchTable.tsx | 6 ++++-- react-app/src/components/projects/ProjectDialog.tsx | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/react-app/src/components/projects/AgencyResponseSearchTable.tsx b/react-app/src/components/projects/AgencyResponseSearchTable.tsx index 53b3f4c47..848ab678d 100644 --- a/react-app/src/components/projects/AgencyResponseSearchTable.tsx +++ b/react-app/src/components/projects/AgencyResponseSearchTable.tsx @@ -71,6 +71,8 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { field: 'Note', headerName: 'Note', flex: 1, + minWidth: 200, + maxWidth: 400, editable: edit, type: 'string', renderCell: (params) => @@ -83,7 +85,7 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { { field: 'Response', headerName: 'Response', - flex: 1, + flex: 0.4, editable: edit, type: 'singleSelect', valueOptions: Object.keys(AgencyResponseType).filter((key) => isNaN(Number(key))), @@ -92,7 +94,7 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { return ( row.Id} - autoHeight + editMode="row" hideFooter disableRowSelectionOnClick columns={[...columns, ...(props.additionalColumns ?? [])]} diff --git a/react-app/src/components/projects/ProjectDialog.tsx b/react-app/src/components/projects/ProjectDialog.tsx index d4522bc31..c4c4edbc9 100644 --- a/react-app/src/components/projects/ProjectDialog.tsx +++ b/react-app/src/components/projects/ProjectDialog.tsx @@ -505,7 +505,7 @@ export const ProjectAgencyResponseDialog = (props: IProjectAgencyResponseDialog) }, [initialValues, lookupData?.Agencies]); return ( onCancel()} > - + Date: Wed, 16 Jul 2025 14:32:24 -0700 Subject: [PATCH 05/10] prevent frontend user from setting subscribe under certain conditions --- .../projects/AgencyResponseSearchTable.tsx | 54 ++++++++++++++++++- .../src/constants/agencyResponseTypes.ts | 5 ++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/react-app/src/components/projects/AgencyResponseSearchTable.tsx b/react-app/src/components/projects/AgencyResponseSearchTable.tsx index 848ab678d..28fd66606 100644 --- a/react-app/src/components/projects/AgencyResponseSearchTable.tsx +++ b/react-app/src/components/projects/AgencyResponseSearchTable.tsx @@ -11,13 +11,14 @@ import { SxProps, IconButton, useTheme, + Tooltip, } from '@mui/material'; import { GridColDef, DataGrid, DataGridProps } from '@mui/x-data-grid'; import { useState } from 'react'; import { ISelectMenuItem } from '../form/SelectFormField'; import { Agency } from '@/hooks/api/useAgencyApi'; import { dateFormatter } from '@/utilities/formatters'; -import { AgencyResponseType } from '@/constants/agencyResponseTypes'; +import { AgencyResponseType, AgencyResponseTypeLabels } from '@/constants/agencyResponseTypes'; import { enumReverseLookup } from '@/utilities/helperFunctions'; interface IAgencySearchTable { @@ -86,9 +87,43 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { field: 'Response', headerName: 'Response', flex: 0.4, + minWidth: 125, editable: edit, type: 'singleSelect', valueOptions: Object.keys(AgencyResponseType).filter((key) => isNaN(Number(key))), + renderCell: (params) => { + // If the agency is disabled or SendEmail is false, and the response is Unsubscribe, disable the cell + // This provides a visual cue that the agency cannot be set to subscribe + // Works in tandem with the isCellEditable function below + if (params.row.Name === 'Finance') console.log('params.row', params.row); + const isDisabled = + (params.row.IsDisabled || !params.row.SendEmail) && + params.row.Response === AgencyResponseTypeLabels[AgencyResponseType.Unsubscribe]; + if (isDisabled) { + return ( + + + {params.value || ''} + + + ); + } + return ( + + {params.value || ''} + + ); + }, }, ]; return ( @@ -97,6 +132,7 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { editMode="row" hideFooter disableRowSelectionOnClick + disableColumnResize columns={[...columns, ...(props.additionalColumns ?? [])]} sx={{ ...props.sx, @@ -105,6 +141,22 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { }, }} rows={props.rows} + isCellEditable={(params) => { + // Prevent editing Response column if agency is disabled or SendEmail is false AND response is Unsubscribe + // This allows the user to see the response but not change it + // Works in tandem with the renderCell function above + if (params.field === 'Response') { + if ( + (params.row.IsDisabled || !params.row.SendEmail) && + params.row.Response === AgencyResponseTypeLabels[AgencyResponseType.Unsubscribe] + ) { + return false; + } + return edit; + } + // For other columns, use their editable property + return params.colDef.editable === true; + }} {...props.dataGridProps} /> ); diff --git a/react-app/src/constants/agencyResponseTypes.ts b/react-app/src/constants/agencyResponseTypes.ts index ff7bdfb23..a520ab435 100644 --- a/react-app/src/constants/agencyResponseTypes.ts +++ b/react-app/src/constants/agencyResponseTypes.ts @@ -2,3 +2,8 @@ export enum AgencyResponseType { Unsubscribe = 0, Subscribe = 1, } + +export const AgencyResponseTypeLabels: Record = { + [AgencyResponseType.Unsubscribe]: 'Unsubscribe', + [AgencyResponseType.Subscribe]: 'Subscribe', +}; From defef979616db3a2390657b290c9d1abab17cb68 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 16 Jul 2025 14:42:19 -0700 Subject: [PATCH 06/10] initial agency intrest sort state --- .../components/projects/AgencyResponseSearchTable.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/react-app/src/components/projects/AgencyResponseSearchTable.tsx b/react-app/src/components/projects/AgencyResponseSearchTable.tsx index 28fd66606..45af47ae9 100644 --- a/react-app/src/components/projects/AgencyResponseSearchTable.tsx +++ b/react-app/src/components/projects/AgencyResponseSearchTable.tsx @@ -13,7 +13,7 @@ import { useTheme, Tooltip, } from '@mui/material'; -import { GridColDef, DataGrid, DataGridProps } from '@mui/x-data-grid'; +import { GridColDef, DataGrid, DataGridProps, useGridApiRef } from '@mui/x-data-grid'; import { useState } from 'react'; import { ISelectMenuItem } from '../form/SelectFormField'; import { Agency } from '@/hooks/api/useAgencyApi'; @@ -46,6 +46,7 @@ interface IAgencySimpleTable { export const AgencySimpleTable = (props: IAgencySimpleTable) => { const theme = useTheme(); + const apiRef = useGridApiRef(); const edit = props.editMode; const columns: GridColDef[] = [ { @@ -128,6 +129,7 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { ]; return ( row.Id} editMode="row" hideFooter @@ -157,6 +159,11 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { // For other columns, use their editable property return params.colDef.editable === true; }} + initialState={{ + sorting: { + sortModel: [{ field: 'Name', sort: 'asc' }], + }, + }} {...props.dataGridProps} /> ); From 66e212f531c8e07ad3d86780e73d2323940d5c40 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 16 Jul 2025 15:13:27 -0700 Subject: [PATCH 07/10] return to cell edit mode due to saving complications --- .../src/components/projects/AgencyResponseSearchTable.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/react-app/src/components/projects/AgencyResponseSearchTable.tsx b/react-app/src/components/projects/AgencyResponseSearchTable.tsx index 45af47ae9..56ccb34ce 100644 --- a/react-app/src/components/projects/AgencyResponseSearchTable.tsx +++ b/react-app/src/components/projects/AgencyResponseSearchTable.tsx @@ -13,7 +13,7 @@ import { useTheme, Tooltip, } from '@mui/material'; -import { GridColDef, DataGrid, DataGridProps, useGridApiRef } from '@mui/x-data-grid'; +import { GridColDef, DataGrid, DataGridProps } from '@mui/x-data-grid'; import { useState } from 'react'; import { ISelectMenuItem } from '../form/SelectFormField'; import { Agency } from '@/hooks/api/useAgencyApi'; @@ -46,7 +46,6 @@ interface IAgencySimpleTable { export const AgencySimpleTable = (props: IAgencySimpleTable) => { const theme = useTheme(); - const apiRef = useGridApiRef(); const edit = props.editMode; const columns: GridColDef[] = [ { @@ -129,9 +128,8 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { ]; return ( row.Id} - editMode="row" + editMode="cell" hideFooter disableRowSelectionOnClick disableColumnResize From 763eedaf54e6f5bfd4ae1cb845b7a1084d31803d Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Thu, 17 Jul 2025 15:25:03 -0700 Subject: [PATCH 08/10] update test cases --- .../projects/projectsController.test.ts | 34 ++++- .../projects/projectsServices.test.ts | 142 +++++++++++++++++- 2 files changed, 166 insertions(+), 10 deletions(-) diff --git a/express-api/tests/unit/controllers/projects/projectsController.test.ts b/express-api/tests/unit/controllers/projects/projectsController.test.ts index 312a26f06..80d3e0a96 100644 --- a/express-api/tests/unit/controllers/projects/projectsController.test.ts +++ b/express-api/tests/unit/controllers/projects/projectsController.test.ts @@ -52,9 +52,12 @@ const _updateProjectAgencyResponses = jest .fn() // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementation((_id: number, responses: ProjectAgencyResponse[], user: User) => { - return responses.map((r) => - produceNotificationQueue({ ProjectId: r.ProjectId, ToAgencyId: r.AgencyId }), - ) as NotificationQueue[]; + return { + sentNotifications: responses.map((r) => + produceNotificationQueue({ ProjectId: r.ProjectId, ToAgencyId: r.AgencyId }), + ) as NotificationQueue[], + invalidResponseChanges: 0, + }; }); jest @@ -426,7 +429,7 @@ describe('UNIT - Testing controllers for users routes.', () => { mockRequest.params.projectId = '1'; await controllers.updateProjectAgencyResponses(mockRequest, mockResponse); expect(mockResponse.statusValue).toBe(200); - expect(mockResponse.sendValue).toHaveLength(1); + expect(mockResponse.sendValue.sentNotifications).toHaveLength(1); }); it('should return 400 when the body of responses is not properly formatted', async () => { @@ -439,7 +442,9 @@ describe('UNIT - Testing controllers for users routes.', () => { }); it('should return 400 when the projectId is not a number', async () => { - mockRequest.body = { responses: [produceAgencyResponse()] }; + mockRequest.body = { + responses: [produceAgencyResponse({ Response: 0 })], // Response 0 is Subscribe + }; mockRequest.setPimsUser({ RoleId: Roles.ADMIN, hasOneOfRoles: () => true }); mockRequest.params.projectId = 'a'; await controllers.updateProjectAgencyResponses(mockRequest, mockResponse); @@ -447,6 +452,25 @@ describe('UNIT - Testing controllers for users routes.', () => { expect(mockResponse.sendValue).toBe('Invalid Project ID'); }); + it('should return 400 when some invalid subscribe requests are received', async () => { + mockRequest.body = { + responses: [produceAgencyResponse({ Response: 0 })], // Response 0 is Subscribe + }; + mockRequest.setPimsUser({ RoleId: Roles.ADMIN, hasOneOfRoles: () => true }); + mockRequest.params.projectId = '1'; + _updateProjectAgencyResponses.mockImplementationOnce(() => { + return { + sentNotifications: [], + invalidResponseChanges: 1, + }; + }); + await controllers.updateProjectAgencyResponses(mockRequest, mockResponse); + expect(mockResponse.statusValue).toBe(400); + expect(mockResponse.sendValue).toBe( + '1 of the responses were not updated due to invalid changes.', + ); + }); + it('should return 403 when a non-admin attempts to make this change', async () => { mockRequest.body = { responses: [produceAgencyResponse()] }; mockRequest.setPimsUser({ RoleId: Roles.GENERAL_USER, hasOneOfRoles: () => false }); diff --git a/express-api/tests/unit/services/projects/projectsServices.test.ts b/express-api/tests/unit/services/projects/projectsServices.test.ts index 47f9395e9..c5ff7c0a6 100644 --- a/express-api/tests/unit/services/projects/projectsServices.test.ts +++ b/express-api/tests/unit/services/projects/projectsServices.test.ts @@ -889,9 +889,6 @@ describe('UNIT - Project Services', () => { }); describe('updateProjectAgencyResponses', () => { - jest - .spyOn(AppDataSource.getRepository(Agency), 'findOne') - .mockImplementation(async () => produceAgency()); jest .spyOn(AppDataSource.getRepository(ProjectNote), 'find') .mockImplementation(async () => [produceNote()]); @@ -908,18 +905,153 @@ describe('UNIT - Project Services', () => { jest .spyOn(AppDataSource.getRepository(ProjectAgencyResponse), 'save') .mockImplementation(async () => produceAgencyResponse()); + it('should return the list of sent notifications from this update', async () => { + const returnAgency = produceAgency({ + SendEmail: true, + Email: 'email@email.com', + IsDisabled: false, + }); + jest + .spyOn(AppDataSource.getRepository(Agency), 'findOne') + .mockImplementationOnce(async () => returnAgency); + jest + .spyOn(AppDataSource.getRepository(Agency), 'find') + .mockImplementationOnce(async () => [returnAgency]); + const project = produceProject(); - const responses = [produceAgencyResponse({ Response: AgencyResponseType.Subscribe })]; + const responses = [ + produceAgencyResponse({ + Response: AgencyResponseType.Subscribe, + AgencyId: returnAgency.Id, + }), + ]; const user = producePimsRequestUser(); const result = await projectServices.updateProjectAgencyResponses( project.Id, responses, user, ); - expect(result).toHaveLength( + expect(result.sentNotifications).toHaveLength( responses.filter((r) => r.Response === AgencyResponseType.Subscribe).length, ); + expect(result.invalidResponseChanges).toBe(0); + }); + + it('should return a invalid response of 1 if the agency cannot be found', async () => { + const returnAgency = produceAgency({ + SendEmail: true, + Email: 'email@email.com', + IsDisabled: false, + }); + jest + .spyOn(AppDataSource.getRepository(Agency), 'findOne') + .mockImplementationOnce(async () => returnAgency); + jest + .spyOn(AppDataSource.getRepository(Agency), 'find') + .mockImplementationOnce(async () => []); + + const project = produceProject(); + const responses = [ + produceAgencyResponse({ + Response: AgencyResponseType.Subscribe, + AgencyId: returnAgency.Id, + }), + ]; + const user = producePimsRequestUser(); + const result = await projectServices.updateProjectAgencyResponses( + project.Id, + responses, + user, + ); + expect(result.invalidResponseChanges).toBe(1); + }); + + it('should return a invalid response of 1 if the agency is disabled', async () => { + const returnAgency = produceAgency({ + SendEmail: true, + Email: 'email@email.com', + IsDisabled: true, + }); + jest + .spyOn(AppDataSource.getRepository(Agency), 'findOne') + .mockImplementationOnce(async () => returnAgency); + jest + .spyOn(AppDataSource.getRepository(Agency), 'find') + .mockImplementationOnce(async () => [returnAgency]); + + const project = produceProject(); + const responses = [ + produceAgencyResponse({ + Response: AgencyResponseType.Subscribe, + AgencyId: returnAgency.Id, + }), + ]; + const user = producePimsRequestUser(); + const result = await projectServices.updateProjectAgencyResponses( + project.Id, + responses, + user, + ); + expect(result.invalidResponseChanges).toBe(1); + }); + + it('should return a invalid response of 1 if the agency has SendEmail disabled', async () => { + const returnAgency = produceAgency({ + SendEmail: false, + Email: 'email@email.com', + IsDisabled: false, + }); + jest + .spyOn(AppDataSource.getRepository(Agency), 'findOne') + .mockImplementationOnce(async () => returnAgency); + jest + .spyOn(AppDataSource.getRepository(Agency), 'find') + .mockImplementationOnce(async () => [returnAgency]); + + const project = produceProject(); + const responses = [ + produceAgencyResponse({ + Response: AgencyResponseType.Subscribe, + AgencyId: returnAgency.Id, + }), + ]; + const user = producePimsRequestUser(); + const result = await projectServices.updateProjectAgencyResponses( + project.Id, + responses, + user, + ); + expect(result.invalidResponseChanges).toBe(1); + }); + + it('should return a invalid response of 1 if the agency has a blank email', async () => { + const returnAgency = produceAgency({ + SendEmail: true, + Email: '', + IsDisabled: false, + }); + jest + .spyOn(AppDataSource.getRepository(Agency), 'findOne') + .mockImplementationOnce(async () => returnAgency); + jest + .spyOn(AppDataSource.getRepository(Agency), 'find') + .mockImplementationOnce(async () => [returnAgency]); + + const project = produceProject(); + const responses = [ + produceAgencyResponse({ + Response: AgencyResponseType.Subscribe, + AgencyId: returnAgency.Id, + }), + ]; + const user = producePimsRequestUser(); + const result = await projectServices.updateProjectAgencyResponses( + project.Id, + responses, + user, + ); + expect(result.invalidResponseChanges).toBe(1); }); }); }); From 80fdf116b0cebd2803014fa52a01a3dc2c6662c6 Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Wed, 5 Nov 2025 16:26:10 -0800 Subject: [PATCH 09/10] ensure lookup is refreshed on agency notification settings edit --- react-app/src/components/agencies/AgencyDetails.tsx | 1 + react-app/src/components/projects/AgencyResponseSearchTable.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/react-app/src/components/agencies/AgencyDetails.tsx b/react-app/src/components/agencies/AgencyDetails.tsx index 227b696bd..79d4efaf4 100644 --- a/react-app/src/components/agencies/AgencyDetails.tsx +++ b/react-app/src/components/agencies/AgencyDetails.tsx @@ -276,6 +276,7 @@ const AgencyDetail = ({ onClose }: IAgencyDetail) => { SendEmail, }).then(() => { refreshData(); + refreshLookup(); setOpenNotificationsDialog(false); }); } diff --git a/react-app/src/components/projects/AgencyResponseSearchTable.tsx b/react-app/src/components/projects/AgencyResponseSearchTable.tsx index 56ccb34ce..30d5aaef4 100644 --- a/react-app/src/components/projects/AgencyResponseSearchTable.tsx +++ b/react-app/src/components/projects/AgencyResponseSearchTable.tsx @@ -95,7 +95,6 @@ export const AgencySimpleTable = (props: IAgencySimpleTable) => { // If the agency is disabled or SendEmail is false, and the response is Unsubscribe, disable the cell // This provides a visual cue that the agency cannot be set to subscribe // Works in tandem with the isCellEditable function below - if (params.row.Name === 'Finance') console.log('params.row', params.row); const isDisabled = (params.row.IsDisabled || !params.row.SendEmail) && params.row.Response === AgencyResponseTypeLabels[AgencyResponseType.Unsubscribe]; From cc4e27970efa329fc602a95d1de4b53338c7d69c Mon Sep 17 00:00:00 2001 From: dbarkowsky Date: Fri, 7 Nov 2025 11:25:29 -0800 Subject: [PATCH 10/10] add patch-package and bump node versions --- express-api/Dockerfile | 2 +- react-app/Dockerfile | 2 +- react-app/package.json | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/express-api/Dockerfile b/express-api/Dockerfile index 4eefa2a0e..4402a5581 100644 --- a/express-api/Dockerfile +++ b/express-api/Dockerfile @@ -22,7 +22,7 @@ RUN npm run build ############################################# # Prod Build # ############################################# -FROM node:22.9-bullseye-slim as Prod +FROM node:22.15-bullseye-slim as Prod # Set the working directory to /express-api WORKDIR /express-api diff --git a/react-app/Dockerfile b/react-app/Dockerfile index 9eff48f98..a3251808a 100644 --- a/react-app/Dockerfile +++ b/react-app/Dockerfile @@ -1,7 +1,7 @@ ############################################# # Base Build # ############################################# -FROM node:22.9-bullseye-slim as base +FROM node:22.15-bullseye-slim as base # Set the working directory to /app WORKDIR /app diff --git a/react-app/package.json b/react-app/package.json index c9be3eafd..106c27139 100644 --- a/react-app/package.json +++ b/react-app/package.json @@ -58,6 +58,7 @@ "eslint-plugin-react": "7.37.1", "jest": "^29.7.0", "jest-environment-jsdom": "29.7.0", + "patch-package": "8.0.1", "prettier": "3.6.2", "react-test-renderer": "18.3.1", "ts-jest": "29.4.0",