diff --git a/.circleci/config.yml b/.circleci/config.yml index c291639ad5..3c1bd4d932 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -560,7 +560,7 @@ parameters: type: string dev_git_branch: # change to feature branch to test deployment description: "Name of github branch that will deploy to dev" - default: "TTAHUB-3097/transactions-for-workers-round-2" + default: "al-ttahub-3196-new-tr-views" type: string sandbox_git_branch: # change to feature branch to test deployment default: "mb/TTAHUB-3198/training-report-alerts" diff --git a/.gitignore b/.gitignore index 82fa02f30c..5565f171cc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ tests/api/report tests/e2e/report tests/e2e/test-results tests/api/test-results +test-results # Ignore /doc => /docs symbolic link created for adr-tools /doc diff --git a/email_templates/tr_collaborator_added/html.pug b/email_templates/tr_collaborator_added/html.pug index 9865e0af79..7fb9d68fa3 100644 --- a/email_templates/tr_collaborator_added/html.pug +++ b/email_templates/tr_collaborator_added/html.pug @@ -2,7 +2,7 @@ style include ../email.css p Hello, p You've been added as an event collaborator on Training Report #{displayId}. -p Access this report in the TTA Hub: +p Select the link below to review and submit the event details in the TTA Hub: ul li a(href=reportPath)= displayId diff --git a/email_templates/tr_collaborator_added/subject.pug b/email_templates/tr_collaborator_added/subject.pug index b9a217366e..74046e2e1e 100644 --- a/email_templates/tr_collaborator_added/subject.pug +++ b/email_templates/tr_collaborator_added/subject.pug @@ -1 +1 @@ -= `You have been added to Training Report ${displayId}` += `Action needed: Submit event details for Training Report ${displayId}` diff --git a/email_templates/tr_collaborator_reminder_event/html.pug b/email_templates/tr_collaborator_reminder_event/html.pug new file mode 100644 index 0000000000..08c428f394 --- /dev/null +++ b/email_templates/tr_collaborator_reminder_event/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p This is a reminder that you have been assigned as an Event Collaborator on Training Report #{displayId}. +p Select the link below to access the report in the TTA Hub: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_collaborator_reminder_event/subject.pug b/email_templates/tr_collaborator_reminder_event/subject.pug new file mode 100644 index 0000000000..c10006eb4c --- /dev/null +++ b/email_templates/tr_collaborator_reminder_event/subject.pug @@ -0,0 +1 @@ += `${prefix} Submit event details for Training Report ${displayId}` diff --git a/email_templates/tr_collaborator_reminder_no_sessions/html.pug b/email_templates/tr_collaborator_reminder_no_sessions/html.pug new file mode 100644 index 0000000000..067e539d20 --- /dev/null +++ b/email_templates/tr_collaborator_reminder_no_sessions/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p This is a reminder that you have been assigned as an Event Collaborator on Training Report #{displayId}. +p Select the link below to create a session in the TTA Hub: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_collaborator_reminder_no_sessions/subject.pug b/email_templates/tr_collaborator_reminder_no_sessions/subject.pug new file mode 100644 index 0000000000..0d356bd696 --- /dev/null +++ b/email_templates/tr_collaborator_reminder_no_sessions/subject.pug @@ -0,0 +1 @@ += `${prefix} Create a session for Training Report ${displayId}` diff --git a/email_templates/tr_collaborator_reminder_session/html.pug b/email_templates/tr_collaborator_reminder_session/html.pug new file mode 100644 index 0000000000..e0c5b2c72d --- /dev/null +++ b/email_templates/tr_collaborator_reminder_session/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p This is a reminder that you have been assigned as an Event Collaborator on Training Report #{displayId}. +p Select the link below to review and submit the session details in the TTA Hub: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_collaborator_reminder_session/subject.pug b/email_templates/tr_collaborator_reminder_session/subject.pug new file mode 100644 index 0000000000..21fb86def0 --- /dev/null +++ b/email_templates/tr_collaborator_reminder_session/subject.pug @@ -0,0 +1 @@ += `${prefix} Submit session details for Training Report ${displayId}` diff --git a/email_templates/tr_event_complete/html.pug b/email_templates/tr_event_complete/html.pug index f590f41567..d29c86cc1c 100644 --- a/email_templates/tr_event_complete/html.pug +++ b/email_templates/tr_event_complete/html.pug @@ -2,7 +2,7 @@ style include ../email.css p Hello, p Training Report #{displayId} has been completed. -p Access this report in the TTA Hub: +p Select the link below to access the report in the TTA Hub: ul li a(href=reportPath)= displayId diff --git a/email_templates/tr_event_complete/subject.pug b/email_templates/tr_event_complete/subject.pug index ffe928f68e..2bbf02de43 100644 --- a/email_templates/tr_event_complete/subject.pug +++ b/email_templates/tr_event_complete/subject.pug @@ -1 +1 @@ -= `A Training Report has been completed` += `Training Report ${displayId} has been completed!` diff --git a/email_templates/tr_event_imported/html.pug b/email_templates/tr_event_imported/html.pug new file mode 100644 index 0000000000..8f09f8a691 --- /dev/null +++ b/email_templates/tr_event_imported/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p You've been assigned as an Event Creator on Training Report #{displayId}. +p Select the link below to review and submit the event details in the TTA Hub:: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_event_imported/subject.pug b/email_templates/tr_event_imported/subject.pug new file mode 100644 index 0000000000..fbe0330861 --- /dev/null +++ b/email_templates/tr_event_imported/subject.pug @@ -0,0 +1 @@ += `Action needed: Submit event details for Training Report ${displayId}` \ No newline at end of file diff --git a/email_templates/tr_owner_reminder_event/html.pug b/email_templates/tr_owner_reminder_event/html.pug new file mode 100644 index 0000000000..ae1f1d2a80 --- /dev/null +++ b/email_templates/tr_owner_reminder_event/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p This is a reminder that you have been assigned as an Event Creator on Training Report #{displayId}. +p Select the link below to access the report in the TTA Hub: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_owner_reminder_event/subject.pug b/email_templates/tr_owner_reminder_event/subject.pug new file mode 100644 index 0000000000..581592f9bc --- /dev/null +++ b/email_templates/tr_owner_reminder_event/subject.pug @@ -0,0 +1 @@ += `${prefix} Submit event details for Training Report ${displayId}` diff --git a/email_templates/tr_owner_reminder_event_not_completed/html.pug b/email_templates/tr_owner_reminder_event_not_completed/html.pug new file mode 100644 index 0000000000..40347bec92 --- /dev/null +++ b/email_templates/tr_owner_reminder_event_not_completed/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p This is a reminder that Training Report #{displayId} is ready for your review. +p Select the link below to review and complete the event in the TTA Hub: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_owner_reminder_event_not_completed/subject.pug b/email_templates/tr_owner_reminder_event_not_completed/subject.pug new file mode 100644 index 0000000000..ecf281663f --- /dev/null +++ b/email_templates/tr_owner_reminder_event_not_completed/subject.pug @@ -0,0 +1 @@ += `${prefix} Complete Training Report ${displayId}` diff --git a/email_templates/tr_owner_reminder_no_sessions/html.pug b/email_templates/tr_owner_reminder_no_sessions/html.pug new file mode 100644 index 0000000000..f1b5a3199e --- /dev/null +++ b/email_templates/tr_owner_reminder_no_sessions/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p This is a reminder that you have been assigned as an Event Creator on Training Report #{displayId}. +p Select the link below to create a session in the TTA Hub: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_owner_reminder_no_sessions/subject.pug b/email_templates/tr_owner_reminder_no_sessions/subject.pug new file mode 100644 index 0000000000..0d356bd696 --- /dev/null +++ b/email_templates/tr_owner_reminder_no_sessions/subject.pug @@ -0,0 +1 @@ += `${prefix} Create a session for Training Report ${displayId}` diff --git a/email_templates/tr_owner_reminder_session/html.pug b/email_templates/tr_owner_reminder_session/html.pug new file mode 100644 index 0000000000..6ade04e10f --- /dev/null +++ b/email_templates/tr_owner_reminder_session/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p This is a reminder that you have been assigned as an Event Creator on Training Report #{displayId}. +p Select the link below to review and submit the session details in the TTA Hub: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_owner_reminder_session/subject.pug b/email_templates/tr_owner_reminder_session/subject.pug new file mode 100644 index 0000000000..21fb86def0 --- /dev/null +++ b/email_templates/tr_owner_reminder_session/subject.pug @@ -0,0 +1 @@ += `${prefix} Submit session details for Training Report ${displayId}` diff --git a/email_templates/tr_poc_added/html.pug b/email_templates/tr_poc_added/html.pug deleted file mode 100644 index fe3717ba62..0000000000 --- a/email_templates/tr_poc_added/html.pug +++ /dev/null @@ -1,11 +0,0 @@ -style - include ../email.css -p Hello, -p You've been added as a regional point of contact on Training Report #{displayId}. -p Access this report in the TTA Hub: -ul - li - a(href=reportPath)= displayId -p Best Regards, - br - | Your TTA Hub team diff --git a/email_templates/tr_poc_added/subject.pug b/email_templates/tr_poc_added/subject.pug deleted file mode 100644 index b9a217366e..0000000000 --- a/email_templates/tr_poc_added/subject.pug +++ /dev/null @@ -1 +0,0 @@ -= `You have been added to Training Report ${displayId}` diff --git a/email_templates/tr_poc_reminder_session/html.pug b/email_templates/tr_poc_reminder_session/html.pug new file mode 100644 index 0000000000..93e35de975 --- /dev/null +++ b/email_templates/tr_poc_reminder_session/html.pug @@ -0,0 +1,11 @@ +style + include ../email.css +p Hello, +p This is a reminder that you have been assigned as a Regional POC on Training Report #{displayId}. +p Select the link below to review and submit the session details in the TTA Hub: +ul + li + a(href=reportPath)= displayId +p Best Regards, + br + | Your TTA Hub team diff --git a/email_templates/tr_poc_reminder_session/subject.pug b/email_templates/tr_poc_reminder_session/subject.pug new file mode 100644 index 0000000000..21fb86def0 --- /dev/null +++ b/email_templates/tr_poc_reminder_session/subject.pug @@ -0,0 +1 @@ += `${prefix} Submit session details for Training Report ${displayId}` diff --git a/email_templates/tr_poc_session_complete/html.pug b/email_templates/tr_poc_session_complete/html.pug deleted file mode 100644 index 6867d8ffbb..0000000000 --- a/email_templates/tr_poc_session_complete/html.pug +++ /dev/null @@ -1,11 +0,0 @@ -style - include ../email.css -p Hello, -p The POC has completed their portion of the session for Training Report #{displayId}. -p Access this report in the TTA Hub: -ul - li - a(href=reportPath)= displayId -p Best Regards, - br - | Your TTA Hub team diff --git a/email_templates/tr_poc_session_complete/subject.pug b/email_templates/tr_poc_session_complete/subject.pug deleted file mode 100644 index 9b21ccd3f6..0000000000 --- a/email_templates/tr_poc_session_complete/subject.pug +++ /dev/null @@ -1 +0,0 @@ -= `A session has been completed for Training Report ${displayId}` diff --git a/email_templates/tr_poc_vision_complete/html.pug b/email_templates/tr_poc_vision_complete/html.pug deleted file mode 100644 index 4cfc866a5c..0000000000 --- a/email_templates/tr_poc_vision_complete/html.pug +++ /dev/null @@ -1,11 +0,0 @@ -style - include ../email.css -p Hello, -p The POC has completed the vision section of Training Report #{displayId}. -p Access this report in the TTA Hub: -ul - li - a(href=reportPath)= displayId -p Best Regards, - br - | Your TTA Hub team diff --git a/email_templates/tr_poc_vision_complete/subject.pug b/email_templates/tr_poc_vision_complete/subject.pug deleted file mode 100644 index 56b53574a4..0000000000 --- a/email_templates/tr_poc_vision_complete/subject.pug +++ /dev/null @@ -1 +0,0 @@ -= `The vision for Training Report ${displayId} has been completed` diff --git a/email_templates/tr_session_completed/html.pug b/email_templates/tr_session_completed/html.pug deleted file mode 100644 index 3abc51a35e..0000000000 --- a/email_templates/tr_session_completed/html.pug +++ /dev/null @@ -1,11 +0,0 @@ -style - include ../email.css -p Hello, -p A session has been completed for Training Report #{displayId}. -p Access this report in the TTA Hub: -ul - li - a(href=reportPath)= displayId -p Best Regards, - br - | Your TTA Hub team diff --git a/email_templates/tr_session_completed/subject.pug b/email_templates/tr_session_completed/subject.pug deleted file mode 100644 index ad2a9a6168..0000000000 --- a/email_templates/tr_session_completed/subject.pug +++ /dev/null @@ -1 +0,0 @@ -= `A session has been completed for Training Report ${displayId}` \ No newline at end of file diff --git a/email_templates/tr_session_created/html.pug b/email_templates/tr_session_created/html.pug index 49530958f6..4c69352587 100644 --- a/email_templates/tr_session_created/html.pug +++ b/email_templates/tr_session_created/html.pug @@ -1,8 +1,8 @@ style include ../email.css p Hello, -p A session has been created for Training Report #{displayId}. -p Access this report in the TTA Hub: +p You have been added as a Regional POC on #{displayId}. +p Select the link below to review and submit the session details in the TTA Hub: ul li a(href=reportPath)= displayId diff --git a/email_templates/tr_session_created/subject.pug b/email_templates/tr_session_created/subject.pug index 367056edf6..8fc8dca1dc 100644 --- a/email_templates/tr_session_created/subject.pug +++ b/email_templates/tr_session_created/subject.pug @@ -1 +1 @@ -= `A session has been created for Training Report ${displayId}` += `Action needed: Submit session details for Training Report ${displayId}` \ No newline at end of file diff --git a/frontend/src/App.scss b/frontend/src/App.scss index e598069082..341b54644c 100644 --- a/frontend/src/App.scss +++ b/frontend/src/App.scss @@ -445,3 +445,9 @@ fill: #1B1B1B; overflow: hidden; text-overflow: ellipsis; } + +.lead-paragraph { + font-family: "Merriweather Web", Georgia, Cambria, "Times New Roman", Times, serif; + font-size: 1.25rem; + line-height: 1.5; +} diff --git a/frontend/src/__tests__/permissions.js b/frontend/src/__tests__/permissions.js index f296b3a232..3ca508f131 100644 --- a/frontend/src/__tests__/permissions.js +++ b/frontend/src/__tests__/permissions.js @@ -9,6 +9,7 @@ import isAdmin, { canChangeObjectiveStatus, canChangeGoalStatus, canEditOrCreateGoals, + hasTrainingReportWritePermissions, } from '../permissions'; describe('permissions', () => { @@ -376,4 +377,42 @@ describe('permissions', () => { expect(result).toBe(false); }); }); + + describe('hasTrainingReportWritePermissions', () => { + it('returns true if the user has read_write_training_repotrs', () => { + const user = { + permissions: [ + { + scopeId: SCOPE_IDS.READ_WRITE_TRAINING_REPORTS, + regionId: 1, + }, + ], + }; + expect(hasTrainingReportWritePermissions(user)).toBeTruthy(); + }); + + it('returns true if the user has POC training reports', () => { + const user = { + permissions: [ + { + scopeId: SCOPE_IDS.POC_TRAINING_REPORTS, + regionId: 1, + }, + ], + }; + expect(hasTrainingReportWritePermissions(user)).toBeTruthy(); + }); + + it('returns false otherwise', () => { + const user = { + permissions: [ + { + scopeId: SCOPE_IDS.READ_REPORTS, + regionId: 1, + }, + ], + }; + expect(hasTrainingReportWritePermissions(user)).toBeFalsy(); + }); + }); }); diff --git a/frontend/src/components/ApprovedReportSpecialButtons.js b/frontend/src/components/ApprovedReportSpecialButtons.js index 0bb993aa10..653f5cdb75 100644 --- a/frontend/src/components/ApprovedReportSpecialButtons.js +++ b/frontend/src/components/ApprovedReportSpecialButtons.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Grid, ModalToggleButton } from '@trussworks/react-uswds'; +import { Grid, ModalToggleButton, Button } from '@trussworks/react-uswds'; import { canUnlockReports } from '../permissions'; import PrintToPdf from './PrintToPDF'; @@ -9,6 +9,8 @@ export default function ApprovedReportSpecialButtons({ modalRef, user, showUnlockReports, + showCompleteEvent, + onCompleteEvent, }) { const [successfullyCopiedClipboard, setSuccessfullyCopiedClipboard] = useState(false); const [somethingWentWrongWithClipboard, setSomethingWentWrongWithClipboard] = useState(false); @@ -53,12 +55,17 @@ export default function ApprovedReportSpecialButtons({ : null} {navigator && navigator.clipboard - ? + ? : null} + {(showCompleteEvent && onCompleteEvent) ? ( + + ) : null} {showUnlockReports && user && user.permissions && canUnlockReports(user) ? Unlock report : null} @@ -78,6 +85,8 @@ ApprovedReportSpecialButtons.propTypes = { PropTypes.shape(), ]), showUnlockReports: PropTypes.bool, + onCompleteEvent: PropTypes.func, + showCompleteEvent: PropTypes.bool, }; ApprovedReportSpecialButtons.defaultProps = { @@ -85,4 +94,6 @@ ApprovedReportSpecialButtons.defaultProps = { modalRef: null, showUnlockReports: false, user: null, + onCompleteEvent: null, + showCompleteEvent: false, }; diff --git a/frontend/src/pages/ActivityReport/Pages/Review/IncompletePages.css b/frontend/src/components/IncompletePages.css similarity index 100% rename from frontend/src/pages/ActivityReport/Pages/Review/IncompletePages.css rename to frontend/src/components/IncompletePages.css diff --git a/frontend/src/pages/ActivityReport/Pages/Review/IncompletePages.js b/frontend/src/components/IncompletePages.js similarity index 70% rename from frontend/src/pages/ActivityReport/Pages/Review/IncompletePages.js rename to frontend/src/components/IncompletePages.js index 918a1acda1..1f32da15b0 100644 --- a/frontend/src/pages/ActivityReport/Pages/Review/IncompletePages.js +++ b/frontend/src/components/IncompletePages.js @@ -5,13 +5,14 @@ import { Alert } from '@trussworks/react-uswds'; import './IncompletePages.css'; const IncompletePages = ({ + type, incompletePages, }) => ( - Incomplete report + {`Incomplete ${type}`}
- This report cannot be submitted until all sections are complete. - Please review the following sections: + {`This ${type} cannot be submitted until all sections are complete. + Please review the following sections:`}
    {incompletePages.map((page) => (
  • @@ -23,7 +24,12 @@ const IncompletePages = ({ ); IncompletePages.propTypes = { + type: PropTypes.string, incompletePages: PropTypes.arrayOf(PropTypes.string).isRequired, }; +IncompletePages.defaultProps = { + type: 'report', +}; + export default IncompletePages; diff --git a/frontend/src/components/Navigator/__tests__/index.js b/frontend/src/components/Navigator/__tests__/index.js index 6d95dc8b40..3410f9f6b6 100644 --- a/frontend/src/components/Navigator/__tests__/index.js +++ b/frontend/src/components/Navigator/__tests__/index.js @@ -110,6 +110,7 @@ describe('Navigator', () => { formData, onUpdateError, editable, + hideSideNav, }) => { const hookForm = useForm({ defaultValues: formData, @@ -151,6 +152,7 @@ describe('Navigator', () => { isPendingApprover={false} updateShowSavedDraft={jest.fn()} showSavedDraft={false} + hideSideNav={hideSideNav} /> @@ -172,6 +174,7 @@ describe('Navigator', () => { formData = initialData, onUpdateError = jest.fn(), editable = true, + hideSideNav = false, ) => { render( { formData={formData} onUpdateError={onUpdateError} editable={editable} + hideSideNav={hideSideNav} />, ); }; @@ -212,4 +216,49 @@ describe('Navigator', () => { expect(onSaveAndContinue).toHaveBeenCalledTimes(1); }); + + it('hides the side nav when the hideSideNav prop is true', async () => { + const onSaveAndContinue = jest.fn(); + act(() => { + renderNavigator( + onSaveAndContinue, + 'first', + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + defaultPages, + initialData, + jest.fn(), + true, + true, + ); + }); + + // Expect not to find the class 'smart-hub-sidenav-wrapper' in the document. + expect(screen.queryAllByTestId('side-nav').length).toBe(0); + }); + it('shows the side nav when the hideSideNav prop is false', async () => { + const onSaveAndContinue = jest.fn(); + act(() => { + renderNavigator( + onSaveAndContinue, + 'first', + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + defaultPages, + initialData, + jest.fn(), + true, + false, + ); + }); + + // Expect to find the className 'smart-hub-sidenav-wrapper' in the document. + expect(screen.getByTestId('side-nav')).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/Navigator/index.js b/frontend/src/components/Navigator/index.js index 1d90652e3c..c3134a0e63 100644 --- a/frontend/src/components/Navigator/index.js +++ b/frontend/src/components/Navigator/index.js @@ -55,6 +55,7 @@ const Navigator = ({ formDataStatusProp, shouldAutoSave, preFlightForNavigation, + hideSideNav, }) => { const page = useMemo(() => pages.find((p) => p.path === currentPage), [currentPage, pages]); const { isAppLoading, setIsAppLoading, setAppLoadingText } = useContext(AppLoadingContext); @@ -157,7 +158,8 @@ const Navigator = ({ const newLocal = 'smart-hub-sidenav-wrapper no-print'; return ( - + { !hideSideNav && ( + + )} @@ -272,6 +275,7 @@ Navigator.propTypes = { formDataStatusProp: PropTypes.string, shouldAutoSave: PropTypes.bool, preFlightForNavigation: PropTypes.func, + hideSideNav: PropTypes.bool, }; Navigator.defaultProps = { @@ -291,6 +295,7 @@ Navigator.defaultProps = { formDataStatusProp: 'calculatedStatus', shouldAutoSave: true, preFlightForNavigation: () => Promise.resolve(true), + hideSideNav: false, }; export default Navigator; diff --git a/frontend/src/components/PocCompleteCheckbox.js b/frontend/src/components/PocCompleteCheckbox.js deleted file mode 100644 index 1f311ddc08..0000000000 --- a/frontend/src/components/PocCompleteCheckbox.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; -import { Checkbox } from '@trussworks/react-uswds'; -import { useController, useFormContext } from 'react-hook-form'; -import moment from 'moment'; -import UserContext from '../UserContext'; -import isAdmin from '../permissions'; - -export default function PocCompleteCheckbox({ userId, isPoc }) { - const { user } = useContext(UserContext); - const userIsAdmin = isAdmin(user); - const { register, setValue } = useFormContext(); - const { - field: { - onChange: onChangePocComplete, - name: namePocComplete, - value: valuePocComplete, - ref: refPocComplete, - }, - } = useController({ - name: 'pocComplete', - defaultValue: false, - }); - - const onChange = (e) => { - onChangePocComplete(e.target.checked); - - if (e.target.checked) { - setValue('pocCompleteId', userId); - setValue('pocCompleteDate', moment().format('YYYY-MM-DD')); - } else { - setValue('pocCompleteId', null); - setValue('pocCompleteDate', null); - } - }; - - const validEmailRoles = ['ECM', 'GSM', 'TTAC']; - - const hasValidEmailRole = () => { - const userRoles = user.roles.map((r) => r.name); - return userRoles.some((role) => validEmailRoles.includes(role)); - }; - - return ( - <> - {(isPoc && hasValidEmailRole()) || (userIsAdmin) ? ( - <> - - - ) : } - - - - ); -} - -PocCompleteCheckbox.propTypes = { - userId: PropTypes.number.isRequired, - isPoc: PropTypes.bool.isRequired, -}; diff --git a/frontend/src/components/PocCompleteView.js b/frontend/src/components/PocCompleteView.js deleted file mode 100644 index c666118529..0000000000 --- a/frontend/src/components/PocCompleteView.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import moment from 'moment'; -import { Alert } from '@trussworks/react-uswds'; - -export default function PocCompleteView({ - formData, userId, children, reportType, -}) { - const formattedDate = moment(formData.pocCompleteDate, 'YYYY-MM-DD').format('MM/DD/YYYY'); - - let message = `A regional point of contact completed your portion of the ${reportType} report on ${formattedDate} and sent an email to the event creator and collaborator`; - if (userId === Number(formData.pocCompleteId)) { - message = `You completed your portion of the ${reportType} report on ${formattedDate} and sent an email to the event creator and collaborator`; - } - return ( - <> - - {message} - - { children } - - ); -} - -PocCompleteView.propTypes = { - formData: PropTypes.shape({ - pocCompleteDate: PropTypes.string, - pocCompleteId: PropTypes.number, - }).isRequired, - userId: PropTypes.number.isRequired, - children: PropTypes.node.isRequired, - reportType: PropTypes.string, -}; - -PocCompleteView.defaultProps = { - reportType: 'session', -}; diff --git a/frontend/src/components/PrintToPDF.js b/frontend/src/components/PrintToPDF.js index c1e1244f81..29903217d0 100644 --- a/frontend/src/components/PrintToPDF.js +++ b/frontend/src/components/PrintToPDF.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; export default function PrintToPdf({ disabled, className, id }) { - const classes = `usa-button no-print ${className}`; + const classes = `usa-button usa-button--outline no-print ${className}`; return ; } diff --git a/frontend/src/components/ReadOnlyField.js b/frontend/src/components/ReadOnlyField.js index 4b9cae73de..b5face414a 100644 --- a/frontend/src/components/ReadOnlyField.js +++ b/frontend/src/components/ReadOnlyField.js @@ -8,7 +8,7 @@ export default function ReadOnlyField({ label, children }) { return ( <> -

    {label}

    +

    {label}

    {children}

    ); diff --git a/frontend/src/components/SimpleSortableTable.css b/frontend/src/components/SimpleSortableTable.css new file mode 100644 index 0000000000..2374b75fae --- /dev/null +++ b/frontend/src/components/SimpleSortableTable.css @@ -0,0 +1,42 @@ +.ttahub-simple-sortable-table th { + vertical-align: bottom; + position: sticky; + top: 0; + background-color: white; +} + +.ttahub-simple-sortable-table thead th[aria-sort] { + background: white; +} + +.ttahub-simple-sortable-table th>.sortable::after { + display: none; +} + +.ttahub-simple-sortable-table th>.sortable span::after { + content: ""; + background-image: url(../images/sort_both.png); + background-repeat: no-repeat; + background-position: center bottom; + display: inline-block; + position: absolute; + height: 100%; + right: -12px; + bottom: 0; + width: 10px; + height: 20px; +} + +@media(max-width: 1160px) and (min-width: 40em) { + .ttahub-simple-sortable-table th>.sortable span::after { + right: auto + } +} + +.ttahub-simple-sortable-table th>.sortable.asc span::after { + background-image: url(../images/sort_asc.png); +} + +.ttahub-simple-sortable-table th>.sortable.desc span::after { + background-image: url(../images/sort_desc.png); +} \ No newline at end of file diff --git a/frontend/src/components/SimpleSortableTable.js b/frontend/src/components/SimpleSortableTable.js index a264ac5f36..b1f350af58 100644 --- a/frontend/src/components/SimpleSortableTable.js +++ b/frontend/src/components/SimpleSortableTable.js @@ -1,8 +1,14 @@ import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Table } from '@trussworks/react-uswds'; +import './SimpleSortableTable.css'; -const SimpleSortableTable = ({ data, columns, className }) => { +const SimpleSortableTable = ({ + data, + columns, + className, + elementSortProp, +}) => { const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc', @@ -15,10 +21,10 @@ const SimpleSortableTable = ({ data, columns, className }) => { let aValue = a[sortConfig.key]; let bValue = b[sortConfig.key]; if (React.isValidElement(aValue)) { - aValue = aValue.props.children.props['aria-label']; + aValue = aValue.props.children.props[elementSortProp]; } if (React.isValidElement(bValue)) { - bValue = bValue.props.children.props['aria-label']; + bValue = bValue.props.children.props[elementSortProp]; } if (aValue < bValue) { return sortConfig.direction === 'asc' ? -1 : 1; @@ -30,7 +36,7 @@ const SimpleSortableTable = ({ data, columns, className }) => { }); } return sortableItems; - }, [data, sortConfig]); + }, [data, sortConfig, elementSortProp]); const requestSort = (key) => { let direction = 'asc'; @@ -50,7 +56,7 @@ const SimpleSortableTable = ({ data, columns, className }) => { + No, cancel + {}} isApprover={false} @@ -327,6 +521,7 @@ export default function SessionForm({ match }) { showSavedDraft={showSavedDraft} updateShowSavedDraft={updateShowSavedDraft} formDataStatusProp="status" + hideSideNav={!isPoc && !isAdminUser} /> diff --git a/frontend/src/pages/SessionForm/pages.js b/frontend/src/pages/SessionForm/pages.js index 491d918c62..1a3a6e908c 100644 --- a/frontend/src/pages/SessionForm/pages.js +++ b/frontend/src/pages/SessionForm/pages.js @@ -1,15 +1,13 @@ import participants from './pages/participants'; import sessionSummary from './pages/sessionSummary'; import nextSteps from './pages/nextSteps'; -import completeSession from './pages/completeSession'; import supportingAttachments from './pages/supportingAttachments'; -const pages = [ +const pages = { sessionSummary, participants, supportingAttachments, nextSteps, - completeSession, -]; +}; export default pages; diff --git a/frontend/src/pages/SessionForm/pages/__tests__/nextSteps.js b/frontend/src/pages/SessionForm/pages/__tests__/nextSteps.js index f2f153befc..dc0f469471 100644 --- a/frontend/src/pages/SessionForm/pages/__tests__/nextSteps.js +++ b/frontend/src/pages/SessionForm/pages/__tests__/nextSteps.js @@ -1,12 +1,12 @@ +/* eslint-disable react/prop-types */ /* eslint-disable react/jsx-props-no-spreading */ import React from 'react'; -import moment from 'moment'; import { render, screen, act, } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { TRAINING_REPORT_STATUSES } from '@ttahub/common'; import { useForm, FormProvider } from 'react-hook-form'; import nextSteps, { isPageComplete } from '../nextSteps'; import { nextStepsFields } from '../../constants'; @@ -112,7 +112,6 @@ describe('nextSteps', () => { describe('render', () => { const onSaveDraft = jest.fn(); const userId = 1; - const todaysDate = moment().format('YYYY-MM-DD'); const defaultFormValues = { id: 1, @@ -133,8 +132,11 @@ describe('nextSteps', () => { }; const defaultUser = { user: { id: userId, roles: [{ name: 'GSM' }] } }; - // eslint-disable-next-line react/prop-types - const RenderNextSteps = ({ formValues = defaultFormValues, user = defaultUser }) => { + const RenderNextSteps = ({ + formValues = defaultFormValues, + user = defaultUser, + additionalData = null, + }) => { const hookForm = useForm({ mode: 'onBlur', defaultValues: formValues, @@ -149,7 +151,7 @@ describe('nextSteps', () => { {nextSteps.render( - null, + additionalData, formValues, 1, false, @@ -181,21 +183,6 @@ describe('nextSteps', () => { expect(textAreas.length).toBe(2); }); - it('shows checkbox for poc', async () => { - act(() => { - const updatedValues = { - ...defaultFormValues, - event: { pocIds: [userId] }, - }; - - render(); - }); - - expect(await screen.findByLabelText(/Email the event creator and collaborator to let them know my work is complete/i)).toBeVisible(); - }); - it('hides checkbox for poc if roles are invalid', async () => { act(() => { const updatedValues = { @@ -212,73 +199,29 @@ describe('nextSteps', () => { expect(await screen.queryAllByText(/Email the event creator and collaborator to let them know my work is complete/i).length).toBe(0); }); - it('allows selection of checkbox and sets alternate values', async () => { + it('hides the save draft button if the session is complete', async () => { act(() => { - const updatedValues = { + render(); }); - const checkbox = await screen.findByLabelText(/Email the event creator and collaborator to let them know my work is complete/i); - expect(checkbox).not.toBeChecked(); - - act(() => { - userEvent.click(checkbox); - }); - - expect(checkbox).toBeChecked(); - - const hiddenInputs = document.querySelectorAll('input[type="hidden"]'); - expect(hiddenInputs.length).toBe(2); - - const hiddenInputValues = Array.from(hiddenInputs).map((input) => input.value); - expect(hiddenInputValues.includes(todaysDate)).toBe(true); - expect(hiddenInputValues.includes(userId.toString())).toBe(true); + expect(screen.queryByRole('button', { name: /review and submit/i })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /save draft/i })).not.toBeInTheDocument(); }); - it('shows read only for pocs when pocComplete', async () => { + it('shows the save draft button if the session is complete', async () => { act(() => { - const updatedValues = { + render(); }); - - // confirm alert - const alert = await screen.findByText(/sent an email to the event creator and collaborator/i); - expect(alert).toBeVisible(); - - // confirm read-only - const checkbox = screen.queryByRole('checkbox'); - expect(checkbox).not.toBeInTheDocument(); - const textareas = document.querySelectorAll('textarea'); - expect(textareas.length).toBe(0); - - // confirm content - expect(await screen.findByText(/very special note/i)).toBeVisible(); - expect(await screen.findByText('01/01/2022')).toBeVisible(); - expect(await screen.findByText(/other note/i)).toBeVisible(); - expect(await screen.findByText('01/01/2021')).toBeVisible(); + expect(screen.queryByRole('button', { name: /review and submit/i })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /save draft/i })).toBeInTheDocument(); }); }); }); diff --git a/frontend/src/pages/SessionForm/pages/__tests__/participants.js b/frontend/src/pages/SessionForm/pages/__tests__/participants.js index 6405029f96..f3d669f6c7 100644 --- a/frontend/src/pages/SessionForm/pages/__tests__/participants.js +++ b/frontend/src/pages/SessionForm/pages/__tests__/participants.js @@ -12,6 +12,7 @@ import fetchMock from 'fetch-mock'; import { useForm, FormProvider } from 'react-hook-form'; import userEvent from '@testing-library/user-event'; import selectEvent from 'react-select-event'; +import { TRAINING_REPORT_STATUSES } from '@ttahub/common/src/constants'; import participants, { isPageComplete } from '../participants'; import NetworkContext from '../../../../NetworkContext'; import UserContext from '../../../../UserContext'; @@ -91,7 +92,7 @@ describe('participants', () => { }; // eslint-disable-next-line react/prop-types - const RenderParticipants = ({ formValues = defaultFormValues }) => { + const RenderParticipants = ({ formValues = defaultFormValues, additionalData = { status: 'In progress' } }) => { const hookForm = useForm({ mode: 'onBlur', defaultValues: formValues, @@ -106,7 +107,7 @@ describe('participants', () => { {participants.render( - null, + additionalData, formValues, 1, false, @@ -194,41 +195,6 @@ describe('participants', () => { await selectEvent.select(screen.getByLabelText(/Regional Office\/TTA/i), 'TTAC'); }); - it('shows read only mode', async () => { - const readOnlyFormValues = { - ...defaultFormValues, - pocComplete: true, - pocCompleteId: userId, - pocCompleteDate: todaysDate, - event: { - pocIds: [userId], - }, - recipients: [ - { - id: 1, - label: 'R1 R1 G1', - }, - ], - deliveryMethod: 'in-person', - numberOfParticipants: 1, - participants: ['Home Visitor'], - language: ['English'], - }; - - act(() => { - render(); - }); - await waitFor(async () => expect(await screen.findByText('Home Visitor')).toBeVisible()); - - // confirm alert - const alert = await screen.findByText(/sent an email to the event creator and collaborator/i); - expect(alert).toBeVisible(); - - // confirm in-person is capitalized - const inPerson = await screen.findByText('In-person'); - expect(inPerson).toBeVisible(); - }); - it('shows read only mode correctly for hybrid', async () => { const readOnlyFormValues = { ...defaultFormValues, @@ -258,10 +224,6 @@ describe('participants', () => { }); await waitFor(async () => expect(await screen.findByText('Home Visitor')).toBeVisible()); - // confirm alert - const alert = await screen.findByText(/sent an email to the event creator and collaborator/i); - expect(alert).toBeVisible(); - // confirm hybrid is capitalized const inPerson = await screen.findByText('Hybrid'); expect(inPerson).toBeVisible(); @@ -273,7 +235,7 @@ describe('participants', () => { expect(virtuallyLabel).toBeVisible(); }); - it('shows read only mode correctly for ist visit selection', async () => { + it('only shows the continue button when the session status is complete', async () => { const readOnlyFormValues = { ...defaultFormValues, pocComplete: true, @@ -294,21 +256,18 @@ describe('participants', () => { numberOfParticipantsVirtually: 1, participants: ['Home Visitor'], language: ['English'], - isIstVisit: 'yes', - regionalOfficeTta: ['AA', 'TTAC'], + isIstVisit: 'no', }; act(() => { - render(); + render(); }); await waitFor(async () => expect(await screen.findByText('Home Visitor')).toBeVisible()); - - // confirm alert - const alert = await screen.findByText(/sent an email to the event creator and collaborator/i); - expect(alert).toBeVisible(); - - const regionalOfficeTta = await screen.findByText(/aa, ttac/i); - expect(regionalOfficeTta).toBeVisible(); + expect(screen.queryByRole('button', { name: 'Save and continue' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Continue' })).toBeInTheDocument(); }); describe('groups', () => { diff --git a/frontend/src/pages/SessionForm/pages/__tests__/sessionSummary.js b/frontend/src/pages/SessionForm/pages/__tests__/sessionSummary.js index 9f06147af2..9cedd0d9a8 100644 --- a/frontend/src/pages/SessionForm/pages/__tests__/sessionSummary.js +++ b/frontend/src/pages/SessionForm/pages/__tests__/sessionSummary.js @@ -94,7 +94,7 @@ describe('sessionSummary', () => { }; // eslint-disable-next-line react/prop-types - const RenderSessionSummary = ({ formValues = defaultFormValues }) => { + const RenderSessionSummary = ({ formValues = defaultFormValues, additionalData = { status: 'Not started' } }) => { const hookForm = useForm({ mode: 'onBlur', defaultValues: formValues, @@ -109,7 +109,7 @@ describe('sessionSummary', () => { {sessionSummary.render( - null, + additionalData, defaultFormValues, 1, false, @@ -409,5 +409,49 @@ describe('sessionSummary', () => { expect(await screen.findByText(/There was an error fetching objective trainers/i)).toBeInTheDocument(); }); + + it('hides the save draft button if the session status is complete', async () => { + const values = { + ...defaultFormValues, + status: 'Complete', + }; + + render(); + expect(screen.queryByRole('button', { name: /review and submit/i })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /save draft/i })).not.toBeInTheDocument(); + }); + + it('shows the save draft button if the session status is not complete', async () => { + const values = { + ...defaultFormValues, + status: 'In progress', + }; + + render(); + expect(screen.queryByRole('button', { name: /review and submit/i })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /save draft/i })).toBeInTheDocument(); + }); + + it('shows the save and continue button if the admin is editing the session and the session status is not complete', async () => { + const values = { + ...defaultFormValues, + status: 'In progress', + }; + + render(); + expect(screen.queryByRole('button', { name: /save and continue/i })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /review and submit/i })).not.toBeInTheDocument(); + }); + + it('only shows the continue button if the admin is editing the session and the session status is complete', async () => { + const values = { + ...defaultFormValues, + status: 'Complete', + }; + + render(); + expect(screen.queryByRole('button', { name: /continue/i })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /save draft/i })).not.toBeInTheDocument(); + }); }); }); diff --git a/frontend/src/pages/SessionForm/pages/__tests__/supportingAttachments.js b/frontend/src/pages/SessionForm/pages/__tests__/supportingAttachments.js index d2440ab06e..597974fe24 100644 --- a/frontend/src/pages/SessionForm/pages/__tests__/supportingAttachments.js +++ b/frontend/src/pages/SessionForm/pages/__tests__/supportingAttachments.js @@ -51,7 +51,7 @@ describe('supportingAttachments', () => { }; // eslint-disable-next-line react/prop-types - const RenderSupportingAttachments = ({ formValues = defaultFormValues }) => { + const RenderSupportingAttachments = ({ formValues = defaultFormValues, additionalData = { status: 'In progress' } }) => { const hookForm = useForm({ mode: 'onBlur', defaultValues: formValues, @@ -66,7 +66,7 @@ describe('supportingAttachments', () => { {supportingAttachments.render( - null, + additionalData, formValues, 1, false, @@ -96,5 +96,13 @@ describe('supportingAttachments', () => { expect(await screen.findByText(/other non-resource items not available online/i)).toBeVisible(); expect(await screen.findByText(/example: \.doc, \.pdf, \.txt, \.csv \(max size 30 mb\)/i)).toBeVisible(); }); + + it('shows the the coninue button when status is complete', async () => { + act(() => { + render(); + }); + expect(screen.queryByRole('button', { name: 'Save and continue' })).not.toBeInTheDocument(); + expect(await screen.findByText(/continue/i)).toBeVisible(); + }); }); }); diff --git a/frontend/src/pages/SessionForm/pages/nextSteps.js b/frontend/src/pages/SessionForm/pages/nextSteps.js index ec7974bf51..a122831d24 100644 --- a/frontend/src/pages/SessionForm/pages/nextSteps.js +++ b/frontend/src/pages/SessionForm/pages/nextSteps.js @@ -1,109 +1,40 @@ -import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; +import React from 'react'; import moment from 'moment'; import { Helmet } from 'react-helmet'; import { Button, Fieldset, } from '@trussworks/react-uswds'; +import { TRAINING_REPORT_STATUSES } from '@ttahub/common'; import IndicatesRequiredField from '../../../components/IndicatesRequiredField'; import { nextStepsFields, } from '../constants'; import NextStepsRepeater from '../../ActivityReport/Pages/components/NextStepsRepeater'; -import UserContext from '../../../UserContext'; -import PocCompleteView from '../../../components/PocCompleteView'; -import useTrainingReportRole from '../../../hooks/useTrainingReportRole'; -import useTrainingReportTemplateDeterminator from '../../../hooks/useTrainingReportTemplateDeterminator'; -import ReadOnlyField from '../../../components/ReadOnlyField'; -import PocCompleteCheckbox from '../../../components/PocCompleteCheckbox'; -const NextSteps = ({ formData }) => { - const { user } = useContext(UserContext); - const { isPoc } = useTrainingReportRole(formData.event, user.id); - const showReadOnlyView = useTrainingReportTemplateDeterminator(formData, isPoc); - - if (showReadOnlyView) { - return ( - - - Next Steps - - -

    Specialist's next steps

    - { formData.specialistNextSteps.map((step, index) => ( -
    - - {step.note} - - - {step.completeDate} - -
    - ))} - -

    Recipient's next steps

    - { formData.recipientNextSteps.map((step, index) => ( -
    - - {step.note} - - - {step.completeDate} - -
    - ))} -
    - ); - } - - return ( - <> - - Next Steps - - -
    - -
    -
    - -
    - ( + <> + + Next Steps + + +
    + - - ); -}; - -NextSteps.propTypes = { - formData: PropTypes.shape({ - pocComplete: PropTypes.bool, - pocCompleteId: PropTypes.number, - pocCompleteDate: PropTypes.string, - event: PropTypes.shape({ - pocIds: PropTypes.arrayOf(PropTypes.number), - }), - specialistNextSteps: PropTypes.arrayOf(PropTypes.shape({ - note: PropTypes.string, - completeDate: PropTypes.string, - })), - recipientNextSteps: PropTypes.arrayOf(PropTypes.shape({ - note: PropTypes.string, - completeDate: PropTypes.string, - })), - }).isRequired, -}; +
    +
    + +
    + +); const fields = Object.keys(nextStepsFields); const path = 'next-steps'; @@ -138,20 +69,25 @@ export default { formData, _reportId, isAppLoading, - onContinue, + _onContinue, onSaveDraft, onUpdatePage, _weAreAutoSaving, _datePickerKey, - _onFormSubmit, + onFormSubmit, Alert, ) => (
    - - + + { + // if status is 'Completed' then don't show the save draft button. + formData.status !== TRAINING_REPORT_STATUSES.COMPLETE && ( + + ) + }
    diff --git a/frontend/src/pages/SessionForm/pages/participants.js b/frontend/src/pages/SessionForm/pages/participants.js index 396ae0841a..7f59eec8c4 100644 --- a/frontend/src/pages/SessionForm/pages/participants.js +++ b/frontend/src/pages/SessionForm/pages/participants.js @@ -1,9 +1,8 @@ -import React, { - useContext, -} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; +import { TRAINING_REPORT_STATUSES, LANGUAGES } from '@ttahub/common'; import { Helmet } from 'react-helmet'; -import { LANGUAGES } from '@ttahub/common'; + import { useFormContext } from 'react-hook-form'; import useDeepCompareEffect from 'use-deep-compare-effect'; import { @@ -19,11 +18,7 @@ import { import { recipientParticipants } from '../../ActivityReport/constants'; // TODO - move to @ttahub/common import ParticipantsNumberOfParticipants from '../components/ParticipantsNumberOfParticipants'; import FormItem from '../../../components/FormItem'; -import useTrainingReportRole from '../../../hooks/useTrainingReportRole'; -import useTrainingReportTemplateDeterminator from '../../../hooks/useTrainingReportTemplateDeterminator'; -import UserContext from '../../../UserContext'; import RecipientsWithGroups from '../../../components/RecipientsWithGroups'; -import ParticipantsReadOnly from '../components/ParticipantsReadOnly'; const placeholderText = '- Select -'; @@ -52,10 +47,7 @@ const Participants = ({ formData }) => { setValue, } = useFormContext(); - const { user } = useContext(UserContext); - const { isPoc } = useTrainingReportRole(formData.event, user.id); - const showReadOnlyView = useTrainingReportTemplateDeterminator(formData, isPoc); - const isHybrid = watch('deliveryMethod') === 'hybrid'; + const deliveryMethod = watch('deliveryMethod'); const isIstVisit = watch('isIstVisit') === 'yes'; const isNotIstVisit = watch('isIstVisit') === 'no'; @@ -102,15 +94,6 @@ const Participants = ({ formData }) => { } }, [isIstVisit, isNotIstVisit, setValue]); - if (showReadOnlyView) { - return ( - - ); - } - return ( <> @@ -187,17 +170,9 @@ const Participants = ({ formData }) => { )} - {(isIstVisit || isNotIstVisit) && ( - <> - - - )}
    @@ -228,6 +203,13 @@ const Participants = ({ formData }) => { inputRef={register({ required: 'Select one' })} /> + + +
    - - - + + { + additionalData.status !== TRAINING_REPORT_STATUSES.COMPLETE && ( + + ) + } + { + additionalData + && additionalData.isAdminUser && ( + + ) + }
    ), diff --git a/frontend/src/pages/SessionForm/pages/sessionSummary.js b/frontend/src/pages/SessionForm/pages/sessionSummary.js index de9e4ba15c..f03180db61 100644 --- a/frontend/src/pages/SessionForm/pages/sessionSummary.js +++ b/frontend/src/pages/SessionForm/pages/sessionSummary.js @@ -4,7 +4,7 @@ import React, { useContext, useRef, } from 'react'; -import { SUPPORT_TYPES } from '@ttahub/common'; +import { SUPPORT_TYPES, TRAINING_REPORT_STATUSES } from '@ttahub/common'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { @@ -24,11 +24,9 @@ import { ErrorMessage, } from '@trussworks/react-uswds'; import Select from 'react-select'; -import { Link } from 'react-router-dom'; import { getTopics } from '../../../fetchers/topics'; import { getNationalCenters } from '../../../fetchers/nationalCenters'; import IndicatesRequiredField from '../../../components/IndicatesRequiredField'; -import ReadOnlyField from '../../../components/ReadOnlyField'; import ControlledDatePicker from '../../../components/ControlledDatePicker'; import Req from '../../../components/Req'; import selectOptionsReset from '../../../components/selectOptionsReset'; @@ -70,12 +68,7 @@ const SessionSummary = ({ datePickerKey }) => { const data = getValues(); - const { - eventDisplayId, - eventId, - eventName, - id, - } = data; + const { id } = data; const startDate = watch('startDate'); const endDate = watch('endDate'); @@ -172,7 +165,7 @@ const SessionSummary = ({ datePickerKey }) => { name: 'courses', defaultValue: courses || [], rules: { - validate: (value) => (objectiveUseIpdCourses && value.length > 0) || 'Select at least one course', + validate: (value) => !objectiveUseIpdCourses || (objectiveUseIpdCourses && value.length > 0) || 'Select at least one course', }, }); @@ -242,15 +235,6 @@ const SessionSummary = ({ datePickerKey }) => { - - {eventDisplayId} - { /** todo - once the event "view" page is created, convert this to a link to that */} - - - - {eventName} - -
    (
    - - + { + !additionalData.isAdminUser + ? ( + + ) + : + } + { + // if status is 'Completed' then don't show the save draft button. + additionalData + && additionalData.status + && additionalData.status !== TRAINING_REPORT_STATUSES.COMPLETE && ( + + ) + + }
    ), diff --git a/frontend/src/pages/SessionForm/pages/supportingAttachments.js b/frontend/src/pages/SessionForm/pages/supportingAttachments.js index ca15de1f7d..8f9d6c57a9 100644 --- a/frontend/src/pages/SessionForm/pages/supportingAttachments.js +++ b/frontend/src/pages/SessionForm/pages/supportingAttachments.js @@ -3,6 +3,7 @@ import React, { useRef, useState, } from 'react'; +import { TRAINING_REPORT_STATUSES } from '@ttahub/common'; import { ErrorMessage, Fieldset, @@ -15,18 +16,17 @@ import { Helmet } from 'react-helmet'; import { Controller, useFormContext } from 'react-hook-form'; import ReportFileUploader from '../../../components/FileUploader/ReportFileUploader'; import { deleteSessionSupportingAttachment } from '../../../fetchers/File'; -import { pageComplete } from '../constants'; +import { pageComplete, supportingAttachmentsVisitedField } from '../constants'; const path = 'supporting-attachments'; const position = 3; -const visitedField = `pageVisited-${path}`; -const fields = [visitedField]; +const fields = [supportingAttachmentsVisitedField]; const SupportingAttachments = ({ reportId }) => { const [fileError, setFileError] = useState(); const { watch, register, setValue } = useFormContext(); const visitedRef = useRef(false); - const pageVisited = watch(visitedField); + const pageVisited = watch(supportingAttachmentsVisitedField); useEffect(() => { /* @@ -35,7 +35,7 @@ const SupportingAttachments = ({ reportId }) => { */ if (!pageVisited && !visitedRef.current) { visitedRef.current = true; - setValue(visitedField, true); + setValue(supportingAttachmentsVisitedField, true); } }, [pageVisited, setValue]); @@ -44,7 +44,7 @@ const SupportingAttachments = ({ reportId }) => { Supporting Attachments - +
    @@ -94,12 +94,12 @@ export default { reviewSection: () => , review: false, render: ( - _additionalData, + additionalData, _formData, reportId, isAppLoading, onContinue, - _onSaveDraft, + onSaveDraft, onUpdatePage, _weAreAutoSaving, _datePickerKey, @@ -110,7 +110,15 @@ export default {
    - + + { + // if status is 'Completed' then don't show the save draft button. + additionalData + && additionalData.status + && additionalData.status !== TRAINING_REPORT_STATUSES.COMPLETE && ( + + ) + }
    diff --git a/frontend/src/pages/TrainingReportForm/__tests__/index.js b/frontend/src/pages/TrainingReportForm/__tests__/index.js index 4d41957093..e605551824 100644 --- a/frontend/src/pages/TrainingReportForm/__tests__/index.js +++ b/frontend/src/pages/TrainingReportForm/__tests__/index.js @@ -1,5 +1,4 @@ import React from 'react'; -import moment from 'moment'; import { render, screen, act, waitFor, } from '@testing-library/react'; @@ -13,22 +12,51 @@ import AppLoadingContext from '../../../AppLoadingContext'; import { COMPLETE } from '../../../components/Navigator/constants'; import SomethingWentWrong from '../../../SomethingWentWrongContext'; +const completedForm = { + regionId: '1', + reportId: 1, + id: 1, + collaboratorIds: [1, 2, 3], + ownerId: 1, + owner: { + id: 1, name: 'Ted User', email: 'ted.user@computers.always', + }, + pocIds: [1], + data: { + eventId: 'R01-PD-1234', + eventOrganizer: 'IST TTA/Visit', + eventIntendedAudience: 'recipients', + startDate: '01/01/2021', + endDate: '01/01/2021', + trainingType: 'Series', + reasons: ['Reason'], + targetPopulations: ['Target'], + status: 'In progress', + vision: 'asdf', + goal: 'afdf', + eventName: 'E-1 Event', + pageState: { + 1: COMPLETE, + 2: COMPLETE, + }, + }, +}; + describe('TrainingReportForm', () => { const history = createMemoryHistory(); const sessionsUrl = '/api/session-reports/eventId/1234'; const renderTrainingReportForm = ( - trainingReportId, currentPage, + trainingReportId, setErrorResponseCode = jest.fn, + user = { id: 1, permissions: [], name: 'Ted User' }, ) => render( - + @@ -65,7 +93,7 @@ describe('TrainingReportForm', () => { }); act(() => { - renderTrainingReportForm('1', 'event-summary'); + renderTrainingReportForm('1'); }); expect(screen.getByText(/Training report - Event/i)).toBeInTheDocument(); @@ -75,7 +103,7 @@ describe('TrainingReportForm', () => { fetchMock.get('/api/events/id/1', 500); const setErrorResponseCode = jest.fn(); act(() => { - renderTrainingReportForm('1', 'event-summary', setErrorResponseCode); + renderTrainingReportForm('1', setErrorResponseCode); }); await waitFor(() => expect(setErrorResponseCode).toHaveBeenCalledWith(500)); }); @@ -118,7 +146,7 @@ describe('TrainingReportForm', () => { }); act(() => { - renderTrainingReportForm('1', 'event-summary'); + renderTrainingReportForm('1'); }); expect(screen.getByText(/Training report - Event/i)).toBeInTheDocument(); @@ -154,7 +182,7 @@ describe('TrainingReportForm', () => { id: 1, name: 'Ted User', email: 'ted.user@computers.always', }, }); - renderTrainingReportForm('123', 'event-summary'); + renderTrainingReportForm('123'); jest.advanceTimersByTime(30000); expect(fetchMock.called('/api/events/id/123')).toBe(true); @@ -172,61 +200,12 @@ describe('TrainingReportForm', () => { }, }); act(() => { - renderTrainingReportForm('', 'event-summary'); + renderTrainingReportForm(''); }); expect(screen.getByText(/no training report id provided/i)).toBeInTheDocument(); }); - it('tests the on save & continue button', async () => { - fetchMock.get('/api/events/id/123', { - regionId: '1', - reportId: 1, - data: { - eventId: 'R01-PD-1234', - eventIntendedAudience: 'recipients', - eventOrganizer: 'Regional PD Event (with National Centers)', - targetPopulations: ['Infants and Toddlers (ages birth to 3)'], - reasons: ['School Readiness Goals'], - startDate: '01/01/2021', - endDate: '01/01/2021', - }, - collaboratorIds: [1], - ownerId: 1, - pocIds: [1], - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - }); - - act(() => { - renderTrainingReportForm('123', 'event-summary'); - }); - expect(fetchMock.called('/api/events/id/123', { method: 'GET' })).toBe(true); - - await screen.findAllByRole('radio', { checked: true }); - - const updatedAt = moment(); - - fetchMock.put('/api/events/id/123', { - regionId: '1', - reportId: 1, - data: {}, - updatedAt, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - }); - expect(fetchMock.called('/api/events/id/123', { method: 'PUT' })).toBe(false); - const onSaveAndContinueButton = screen.getByText(/save and continue/i); - act(() => { - userEvent.click(onSaveAndContinueButton); - }); - - // check that fetch mock was called with a put request - await waitFor(() => expect(fetchMock.called('/api/events/id/123', { method: 'PUT' })).toBe(true)); - }); - it('tests the on save draft event', async () => { fetchMock.get('/api/events/id/123', { regionId: '1', @@ -238,7 +217,7 @@ describe('TrainingReportForm', () => { }, }); act(() => { - renderTrainingReportForm('123', 'event-summary'); + renderTrainingReportForm('123'); }); expect(fetchMock.called('/api/events/id/123', { method: 'GET' })).toBe(true); @@ -271,7 +250,7 @@ describe('TrainingReportForm', () => { }, }); act(() => { - renderTrainingReportForm('123', 'event-summary'); + renderTrainingReportForm('123'); }); expect(fetchMock.called('/api/events/id/123', { method: 'GET' })).toBe(true); @@ -284,390 +263,45 @@ describe('TrainingReportForm', () => { await waitFor(() => expect(screen.getByText(/There was an error saving the training report. Please try again later./i)).toBeInTheDocument()); }); - it('updates the page via the side menu', async () => { - fetchMock.get('/api/events/id/1', { - id: 1, - name: 'test event', - regionId: '1', - reportId: 1, - collaboratorIds: [], - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - }); - - fetchMock.put('/api/events/id/1', { - regionId: '1', - reportId: 1, - data: {}, - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - }); - - act(() => { - renderTrainingReportForm('1', 'event-summary'); - }); - - expect(screen.getByText(/Training report - Event/i)).toBeInTheDocument(); - - const vision = await screen.findByRole('button', { name: /vision/i }); - - act(() => { - userEvent.click(vision); - }); - - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'PUT' })).toBe(true)); - }); - - it('will update status on submit if the updated status is not complete', async () => { - fetchMock.get('/api/events/id/1', { - id: 1, - name: 'test event', - regionId: '1', - reportId: 1, - collaboratorIds: [], - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - data: { - eventId: 'R01-PD-1234', - }, - }); - - fetchMock.put('/api/events/id/1', { - regionId: '1', - reportId: 1, - data: {}, - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - }); - - fetchMock.get(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - ]); - - act(() => { - renderTrainingReportForm('1', 'complete-event'); - }); - - expect(screen.getByText(/Training report - Event/i)).toBeInTheDocument(); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - expect(statusSelect).toHaveValue('In progress'); - - const submitButton = await screen.findByRole('button', { name: /submit/i }); - act(() => { - userEvent.click(submitButton); - }); - - await waitFor(() => expect(screen.getByText('Event must be complete to submit')).toBeInTheDocument()); - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'PUT' })).toBe(false)); - }); - - it('will not complete the form if the form is not complete', async () => { - fetchMock.get('/api/events/id/1', { - id: 1, - name: 'test event', - regionId: '1', - reportId: 1, - collaboratorIds: [], - ownerId: 1, - data: { - eventId: 'R01-PD-1234', - }, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - }); - - fetchMock.put('/api/events/id/1', { - regionId: '1', - reportId: 1, - data: { - eventId: 'R01-PD-1234', - }, - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - }); - - fetchMock.get(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - ]); - - act(() => { - renderTrainingReportForm('1', 'complete-event'); - }); - - expect(screen.getByText(/Training report - Event/i)).toBeInTheDocument(); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Complete'); - }); - expect(statusSelect).toHaveValue('Complete'); - - const submitButton = await screen.findByRole('button', { name: /submit/i }); - act(() => { - userEvent.click(submitButton); - }); - - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'PUT' })).not.toBe(true)); - }); - - it('will complete the form if the form is complete', async () => { - const completedForm = { - regionId: '1', - reportId: 1, - id: 1, - collaboratorIds: [1, 2, 3], - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - pocIds: [1], - data: { - eventId: 'R01-PD-1234', - eventOrganizer: 'IST TTA/Visit', - eventIntendedAudience: 'recipients', - startDate: '01/01/2021', - endDate: '01/01/2021', - trainingType: 'Series', - reasons: ['Reason'], - targetPopulations: ['Target'], - status: 'In progress', - vision: 'asdf', - eventName: 'E-1 Event', - pageState: { - 1: COMPLETE, - 2: COMPLETE, - }, - }, - }; - + it('handles an error submitting the form', async () => { fetchMock.get('/api/events/id/1', completedForm); - fetchMock.put('/api/events/id/1', completedForm); - fetchMock.get(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - ]); - - act(() => { - renderTrainingReportForm('1', 'complete-event'); - }); - - await waitFor(() => expect(fetchMock.called(sessionsUrl, { method: 'GET' })).toBe(true)); - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'GET' })).toBe(true)); - await waitFor(async () => expect(await screen.findByRole('button', { name: /Event summary complete/i })).toBeInTheDocument()); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Complete'); - }); - expect(statusSelect).toHaveValue('Complete'); - - const submitButton = await screen.findByRole('button', { name: /submit/i }); - act(() => { - userEvent.click(submitButton); - }); - - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'PUT' })).toBe(true)); - const lastBody = JSON.parse(fetchMock.lastOptions().body); - expect(lastBody.data.status).toEqual('Complete'); - }); - - it('shows an error if saving a draft as complete', async () => { - const completedForm = { - regionId: '1', - reportId: 1, - id: 1, - collaboratorIds: [1, 2, 3], - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - pocIds: [1], - data: { - eventOrganizer: 'IST TTA/Visit', - eventIntendedAudience: 'recipients', - startDate: '01/01/2021', - endDate: '01/01/2021', - trainingType: 'Series', - reasons: ['Reason'], - targetPopulations: ['Target'], - status: 'In progress', - vision: 'asdf', - eventId: 'R01-PD-1234', - eventName: 'E-1 Event', - pageState: { - 1: COMPLETE, - 2: COMPLETE, - }, - }, - }; - fetchMock.get('/api/events/id/1', completedForm); - fetchMock.put('/api/events/id/1', completedForm); + fetchMock.put('/api/events/id/1', 500); fetchMock.get(sessionsUrl, [ { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, ]); act(() => { - renderTrainingReportForm('1', 'complete-event'); + renderTrainingReportForm('1'); }); - await waitFor(() => expect(fetchMock.called(sessionsUrl, { method: 'GET' })).toBe(true)); await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'GET' })).toBe(true)); - await waitFor(async () => expect(await screen.findByRole('button', { name: /Event summary complete/i })).toBeInTheDocument()); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Complete'); - }); - expect(statusSelect).toHaveValue('Complete'); - - const submitButton = await screen.findByRole('button', { name: /save draft/i }); - + const submitButton = await screen.findByRole('button', { name: /Review and submit/i }); act(() => { userEvent.click(submitButton); }); - await waitFor(() => expect(screen.getByText('To complete event, submit it')).toBeInTheDocument()); - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'PUT' })).not.toBe(true)); - }); - - it('you can suspend a report', async () => { - const completedForm = { - regionId: '1', - reportId: 1, - id: 1, - collaboratorIds: [1, 2, 3], - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - pocIds: [1], - data: { - eventOrganizer: 'IST TTA/Visit', - eventIntendedAudience: 'recipients', - startDate: '01/01/2021', - endDate: '01/01/2021', - trainingType: 'Series', - reasons: ['Reason'], - targetPopulations: ['Target'], - status: 'In progress', - vision: 'asdf', - eventId: 'R01-PD-1234', - eventName: 'E-1 Event', - pageState: { - 1: COMPLETE, - 2: COMPLETE, - }, - }, - }; - - fetchMock.get('/api/events/id/1', completedForm); - fetchMock.put('/api/events/id/1', completedForm); - fetchMock.get(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - ]); - - act(() => { - renderTrainingReportForm('1', 'complete-event'); - }); - - await waitFor(() => expect(fetchMock.called(sessionsUrl, { method: 'GET' })).toBe(true)); - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'GET' })).toBe(true)); - await waitFor(async () => expect(await screen.findByRole('button', { name: /Event summary complete/i })).toBeInTheDocument()); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Suspended'); - }); - expect(statusSelect).toHaveValue('Suspended'); - - const submitButton = await screen.findByRole('button', { name: /save draft/i }); + // Wait for the modal to display. + await waitFor(() => expect(screen.getByText(/You will not be able to make changes once you save the event./i)).toBeInTheDocument()); + // get the button with the text "Yes, continue". + const yesContinueButton = screen.getByRole('button', { name: /Yes, continue/i }); act(() => { - userEvent.click(submitButton); + userEvent.click(yesContinueButton); }); await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'PUT' })).toBe(true)); - const lastBody = JSON.parse(fetchMock.lastOptions().body); - expect(lastBody.data.status).toEqual('Suspended'); + await waitFor(() => expect(screen.getByText(/There was an error saving the training report/i)).toBeInTheDocument()); }); - it('handles an error submitting the form', async () => { - const completedForm = { - regionId: '1', - reportId: 1, - id: 1, - collaboratorIds: [1, 2, 3], - ownerId: 1, - owner: { - id: 1, name: 'Ted User', email: 'ted.user@computers.always', - }, - pocIds: [1], - data: { - eventId: 'R01-PD-1234', - eventOrganizer: 'IST TTA/Visit', - eventIntendedAudience: 'recipients', - startDate: '01/01/2021', - endDate: '01/01/2021', - trainingType: 'Series', - reasons: ['Reason'], - targetPopulations: ['Target'], - status: 'In progress', - vision: 'asdf', - eventName: 'E-1 Event', - pageState: { - 1: COMPLETE, - 2: COMPLETE, - }, - }, - }; - + it('displays the correct report view link', async () => { fetchMock.get('/api/events/id/1', completedForm); - - fetchMock.put('/api/events/id/1', 500); - fetchMock.get(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - ]); - act(() => { - renderTrainingReportForm('1', 'complete-event'); + renderTrainingReportForm('1'); }); - await waitFor(() => expect(fetchMock.called(sessionsUrl, { method: 'GET' })).toBe(true)); - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'GET' })).toBe(true)); - await waitFor(async () => expect(await screen.findByRole('button', { name: /Event summary complete/i })).toBeInTheDocument()); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Complete'); - }); - expect(statusSelect).toHaveValue('Complete'); - - const submitButton = await screen.findByRole('button', { name: /submit/i }); - act(() => { - userEvent.click(submitButton); - }); - - await waitFor(() => expect(fetchMock.called('/api/events/id/1', { method: 'PUT' })).toBe(true)); - const lastBody = JSON.parse(fetchMock.lastOptions().body); - expect(lastBody.data.status).toEqual('Complete'); - await waitFor(() => expect(screen.getByText(/There was an error saving the training report/i)).toBeInTheDocument()); + await waitFor(() => expect(screen.getByText(/: e-1 event/i)).toBeInTheDocument()); + await waitFor(() => expect(screen.getByText(/r01-pd-1234/i)).toBeInTheDocument()); }); }); diff --git a/frontend/src/pages/TrainingReportForm/index.js b/frontend/src/pages/TrainingReportForm/index.js index 9299bdf2df..f1ca705a30 100644 --- a/frontend/src/pages/TrainingReportForm/index.js +++ b/frontend/src/pages/TrainingReportForm/index.js @@ -4,13 +4,13 @@ import React, { useContext, useRef, } from 'react'; -import moment from 'moment'; import ReactRouterPropTypes from 'react-router-prop-types'; import { Helmet } from 'react-helmet'; -import { Alert, Grid } from '@trussworks/react-uswds'; -import { useHistory, Redirect } from 'react-router-dom'; +import { + Alert, Grid, Form, Button, ModalToggleButton, +} from '@trussworks/react-uswds'; +import { useHistory } from 'react-router-dom'; import { FormProvider, useForm } from 'react-hook-form'; -import useSocket, { usePublishWebsocketLocationOnInterval } from '../../hooks/useSocket'; import useLocalStorage from '../../hooks/useLocalStorage'; import { LOCAL_STORAGE_ADDITIONAL_DATA_KEY, @@ -19,16 +19,11 @@ import { import { getTrainingReportUsers } from '../../fetchers/users'; import { eventById, updateEvent } from '../../fetchers/event'; import NetworkContext, { isOnlineMode } from '../../NetworkContext'; -import UserContext from '../../UserContext'; -import Navigator from '../../components/Navigator'; import BackLink from '../../components/BackLink'; -import pages from './pages'; +import EventSummary from './pages/eventSummary'; import AppLoadingContext from '../../AppLoadingContext'; -import useHookFormPageState from '../../hooks/useHookFormPageState'; import SomethingWentWrongContext from '../../SomethingWentWrongContext'; - -// websocket publish location interval -const INTERVAL_DELAY = 10000; // TEN SECONDS +import Modal from '../../components/VanillaModal'; /** * this is just a simple handler to "flatten" @@ -62,8 +57,9 @@ const resetFormData = (reset, event) => { }; export default function TrainingReportForm({ match }) { - const { params: { currentPage, trainingReportId } } = match; + const { params: { trainingReportId } } = match; const reportId = useRef(); + const modalRef = useRef(); // for redirects if a page is not provided const history = useHistory(); @@ -77,18 +73,12 @@ export default function TrainingReportForm({ match }) { // this error is for errors fetching reports, its the top error const [error, setError] = useState(); - // this is the error that appears in the sidebar - const [errorMessage, updateErrorMessage] = useState(); - /* ============ */ // this attempts to track whether or not we're online // (or at least, if the backend is responding) const [connectionActive] = useState(true); - const [lastSaveTime, updateLastSaveTime] = useState(null); - const [showSavedDraft, updateShowSavedDraft] = useState(false); - /* ============ * this hook handles the interface with * local storage @@ -123,28 +113,9 @@ export default function TrainingReportForm({ match }) { const eventRegion = hookForm.watch('regionId'); const formData = hookForm.getValues(); - - const { user } = useContext(UserContext); const { setIsAppLoading, isAppLoading } = useContext(AppLoadingContext); const { setErrorResponseCode } = useContext(SomethingWentWrongContext); - const { - socket, - setSocketPath, - socketPath, - messageStore, - } = useSocket(user); - - useEffect(() => { - if (!trainingReportId) { - return; - } - const newPath = `/training-reports/${trainingReportId}`; - setSocketPath(newPath); - }, [currentPage, setSocketPath, trainingReportId]); - - usePublishWebsocketLocationOnInterval(socket, socketPath, user, lastSaveTime, INTERVAL_DELAY); - useEffect(() => { const loading = !reportFetched || !additionalDataFetched; setIsAppLoading(loading); @@ -161,7 +132,7 @@ export default function TrainingReportForm({ match }) { const users = await getTrainingReportUsers(eventRegion, trainingReportId); updateAdditionalData({ users }); } catch (e) { - updateErrorMessage('Error fetching collaborators and points of contact'); + setError('Error fetching collaborators and points of contact'); } finally { setAdditionalDataFetched(true); } @@ -173,7 +144,7 @@ export default function TrainingReportForm({ match }) { useEffect(() => { // fetch event report data async function fetchReport() { - if (!trainingReportId || !currentPage || reportFetched) { + if (!trainingReportId || reportFetched) { return; } try { @@ -188,8 +159,7 @@ export default function TrainingReportForm({ match }) { } } fetchReport(); - }, [currentPage, - hookForm.reset, + }, [hookForm.reset, isAppLoading, reportFetched, trainingReportId, @@ -202,33 +172,7 @@ export default function TrainingReportForm({ match }) { } }, [trainingReportId]); - // hook to update the page state in the sidebar - useHookFormPageState(hookForm, pages, currentPage); - - const updatePage = (position) => { - const state = {}; - if (reportId.current) { - state.showLastUpdatedTime = true; - } - - const page = pages.find((p) => p.position === position); - const newPath = `/training-report/${reportId.current}/${page.path}`; - history.push(newPath, state); - }; - - if (!currentPage) { - return ( - - ); - } - - const onSave = async (updatedStatus = null) => { - // if the event is complete, don't allow saving it - if (updatedStatus === 'Complete') { - hookForm.setError('status', { message: 'To complete event, submit it' }); - return; - } - + const onSave = async () => { try { // reset the error message setError(''); @@ -246,26 +190,16 @@ export default function TrainingReportForm({ match }) { } = hookForm.getValues(); const dataToPut = { - data: { - ...data, - }, + data, ownerId: ownerId || null, pocIds: pocIds || null, collaboratorIds, regionId: regionId || null, }; - // autosave sends us a "true" boolean so we don't want to update the status - // if that is the case - if (updatedStatus && typeof updatedStatus === 'string') { - dataToPut.data.status = updatedStatus; - } - // PUT it to the backend const updatedEvent = await updateEvent(trainingReportId, dataToPut); resetFormData(hookForm.reset, updatedEvent); - updateLastSaveTime(moment(updatedEvent.updatedAt)); - updateShowSavedDraft(true); } catch (err) { setError('There was an error saving the training report. Please try again later.'); } finally { @@ -273,17 +207,8 @@ export default function TrainingReportForm({ match }) { } }; - const onSaveAndContinue = async () => { - const whereWeAre = pages.find((p) => p.path === currentPage); - const nextPage = pages.find((p) => p.position === whereWeAre.position + 1); - await onSave(); - updateShowSavedDraft(false); - if (nextPage) { - updatePage(nextPage.position); - } - }; - - const onFormSubmit = async (updatedStatus) => { + const okFormSubmit = async () => { + // Get isValid from the form. try { // reset the error message setError(''); @@ -295,15 +220,13 @@ export default function TrainingReportForm({ match }) { pocIds, collaboratorIds, regionId, + sessionReports, ...data } = hookForm.getValues(); // PUT it to the backend const updatedEvent = await updateEvent(trainingReportId, { - data: { - ...data, - status: updatedStatus, - }, + data, ownerId: ownerId || null, pocIds: pocIds || null, collaboratorIds, @@ -311,20 +234,18 @@ export default function TrainingReportForm({ match }) { }); resetFormData(hookForm.reset, updatedEvent); - updateLastSaveTime(moment(updatedEvent.updatedAt)); - history.push('/training-reports/complete', { message: 'You successfully submitted the event.' }); + // Redirect back based current status tab. + const redirect = updatedEvent.data.status.replace(' ', '-').toLowerCase(); + history.push(`/training-reports/${redirect}`, { message: 'You successfully submitted the event.' }); } catch (err) { + // Close the modal and show the error message. setError('There was an error saving the training report. Please try again later.'); } finally { setIsAppLoading(false); + modalRef.current.toggleModal(false); } }; - const reportCreator = { name: user.name, roles: user.roles }; - - // retrieve the last time the data was saved to local storage - const savedToStorageTime = formData ? formData.savedToStorageTime : null; - const backLinkUrl = (() => { if (!formData || !formData.status) { return '/training-reports/not-started'; @@ -333,11 +254,19 @@ export default function TrainingReportForm({ match }) { return `/training-reports/${formData.status.replace(' ', '-').toLowerCase()}`; })(); + const showSubmitModal = async () => { + const valid = await hookForm.trigger(); + if (valid) { + // Toggle the modal only if the form is valid. + modalRef.current.toggleModal(true); + } + }; + return (
    { error && ( - + {error} )} @@ -347,10 +276,16 @@ export default function TrainingReportForm({ match }) { -
    +

    Training report - Event

    +
    + {formData.eventId} + : + {' '} + {formData.eventName} +
    @@ -363,35 +298,33 @@ export default function TrainingReportForm({ match }) { > {/* eslint-disable-next-line react/jsx-props-no-spreading */} - {}} - isApprover={false} - isPendingApprover={false} - onReview={() => {}} - errorMessage={errorMessage} - updateErrorMessage={updateErrorMessage} - savedToStorageTime={savedToStorageTime} - onSaveDraft={onSave} - onSaveAndContinue={onSaveAndContinue} - showSavedDraft={showSavedDraft} - updateShowSavedDraft={updateShowSavedDraft} - formDataStatusProp="status" - /> + +

    You will not be able to make changes once you save the event.

    + + + No, cancel +
    +
    + +
    diff --git a/frontend/src/pages/TrainingReportForm/pages/__tests__/completeEvent.js b/frontend/src/pages/TrainingReportForm/pages/__tests__/completeEvent.js deleted file mode 100644 index dcac35d4cc..0000000000 --- a/frontend/src/pages/TrainingReportForm/pages/__tests__/completeEvent.js +++ /dev/null @@ -1,348 +0,0 @@ -/* eslint-disable react/jsx-props-no-spreading */ -import React from 'react'; -import { - render, screen, act, waitFor, -} from '@testing-library/react'; -import { Router } from 'react-router'; -import { createMemoryHistory } from 'history'; -import { useForm, FormProvider } from 'react-hook-form'; -import userEvent from '@testing-library/user-event'; -import fetchMock from 'fetch-mock'; -import completeEvent from '../completeEvent'; -import NetworkContext from '../../../../NetworkContext'; -import AppLoadingContext from '../../../../AppLoadingContext'; -import UserContext from '../../../../UserContext'; - -describe('completeEvent', () => { - describe('render', () => { - const defaultPageState = { - 1: 'In progress', - 2: 'Complete', - }; - - const defaultFormValues = { - id: 1, - status: 'Not started', - pageState: defaultPageState, - ownerId: 1, - eventId: 'R01-PD-1234', - }; - - const sessionsUrl = '/api/session-reports/eventId/1234'; - const onSubmit = jest.fn(); - const onSaveForm = jest.fn(); - const onUpdatePage = jest.fn(); - - // eslint-disable-next-line react/prop-types - const RenderCompleteEvent = ({ defaultValues = defaultFormValues }) => { - const hookForm = useForm({ - mode: 'onBlur', - defaultValues, - }); - - const history = createMemoryHistory(); - - const formData = hookForm.watch(); - - return ( - - - - - - {completeEvent.render( - {}, - formData, - 1, - false, - jest.fn(), - onSaveForm, - onUpdatePage, - false, - '', - onSubmit, - () => <>, - )} - - - - - - ); - }; - - afterEach(() => { - fetchMock.reset(); - }); - - it('renders complete event page', async () => { - fetchMock.getOnce(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - { id: 4, eventId: 1, data: { sessionName: '', status: 'Not started' } }, - ]); - - act(() => { - render(); - }); - - expect(await screen.findByRole('cell', { name: /toothbrushing vol 2/i })).toBeInTheDocument(); - expect(await screen.findByRole('cell', { name: /toothbrushing vol 3/i })).toBeInTheDocument(); - expect(await screen.findAllByRole('cell', { name: /complete/i })).toHaveLength(2); // sessions without names are filtered out - - // you can change the status - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Complete'); - }); - expect(statusSelect).toHaveValue('Complete'); - }); - - it('not started and suspended are options when there are no sessions', async () => { - fetchMock.getOnce(sessionsUrl, []); - - act(() => { - render(); - }); - - const statusLabel = await screen.findByText('Event status'); - expect(statusLabel).toBeInTheDocument(); - const status = await screen.findByText('Not started'); - expect(status).toBeInTheDocument(); - - // but there should be no select - const statusSelect = screen.queryByRole('combobox', { name: /status/i }); - expect(statusSelect).not.toBeNull(); - const options = screen.queryAllByRole('option'); - const optionTexts = options.map((option) => option.textContent); - expect(optionTexts).toEqual(['Not started', 'Suspended']); - }); - - it('suspended events change the button text and call "onSave"', async () => { - fetchMock.getOnce(sessionsUrl, []); - - act(() => { - render(); - }); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - - act(() => { - userEvent.selectOptions(statusSelect, 'Suspended'); - }); - - const submitButton = await screen.findByRole('button', { name: /suspend event/i }); - userEvent.click(submitButton); - - expect(onSaveForm).toHaveBeenCalled(); - expect(onSubmit).not.toHaveBeenCalled(); - }); - - it('handles an error fetching sessions', async () => { - fetchMock.getOnce(sessionsUrl, 500); - - act(() => { - render(); - }); - - await waitFor(() => screen.findByText('Unable to load sessions')); - - const statusLabel = await screen.findByText('Event status'); - expect(statusLabel).toBeInTheDocument(); - const status = await screen.findByText('Not started'); - expect(status).toBeInTheDocument(); - - const statusSelect = screen.queryByRole('combobox', { name: /status/i }); - expect(statusSelect).not.toBeNull(); - const options = screen.queryAllByRole('option'); - const optionTexts = options.map((option) => option.textContent); - expect(optionTexts).toEqual(['Not started', 'Suspended']); - }); - - it('calls onUpdatePage when the back button is clicked', async () => { - fetchMock.getOnce(sessionsUrl, []); - - act(() => { - render(); - }); - - const backButton = await screen.findByRole('button', { name: /back/i }); - act(() => { - userEvent.click(backButton); - }); - - // 2 is the complete event page position (3) - 1 - expect(onUpdatePage).toHaveBeenCalledWith(2); - }); - - it('calls saveDraft when the save draft button is clicked', async () => { - fetchMock.getOnce(sessionsUrl, []); - - act(() => { - render(); - }); - - const saveDraftButton = await screen.findByRole('button', { name: /save draft/i }); - act(() => { - userEvent.click(saveDraftButton); - }); - - expect(onSaveForm).toHaveBeenCalled(); - }); - - it('wont submit if there are no sessions', async () => { - fetchMock.getOnce(sessionsUrl, []); - - act(() => { - render(); - }); - - const statusLabel = await screen.findByText('Event status'); - expect(statusLabel).toBeInTheDocument(); - const status = await screen.findByText('Not started'); - expect(status).toBeInTheDocument(); - - const statusSelect = screen.queryByRole('combobox', { name: /status/i }); - expect(statusSelect).not.toBeNull(); - const options = screen.queryAllByRole('option'); - const optionTexts = options.map((option) => option.textContent); - expect(optionTexts).toEqual(['Not started', 'Suspended']); - - const submitButton = await screen.findByRole('button', { name: /submit/i }); - act(() => { - userEvent.click(submitButton); - }); - - expect(onSubmit).not.toHaveBeenCalled(); - }); - - it('wont submit if some sessions aren\'t complete', async () => { - fetchMock.getOnce(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Not started' } }, - ]); - - act(() => { - render(); - }); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Complete'); - }); - expect(statusSelect).toHaveValue('Complete'); - - const submitButton = await screen.findByRole('button', { name: /submit/i }); - act(() => { - userEvent.click(submitButton); - }); - - expect(onSubmit).not.toHaveBeenCalled(); - }); - - it('will not submit if all sessions but not all pages are complete', async () => { - fetchMock.getOnce(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - ]); - - act(() => { - render(); - }); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Complete'); - }); - expect(statusSelect).toHaveValue('Complete'); - - const submitButton = await screen.findByRole('button', { name: /submit/i }); - act(() => { - userEvent.click(submitButton); - }); - - expect(onSubmit).not.toHaveBeenCalled(); - }); - - it('will submit if all sessions and pages are complete', async () => { - fetchMock.getOnce(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - ]); - - act(() => { - render(); - }); - - const statusSelect = await screen.findByRole('combobox', { name: /status/i }); - act(() => { - userEvent.selectOptions(statusSelect, 'Complete'); - }); - expect(statusSelect).toHaveValue('Complete'); - - const submitButton = await screen.findByRole('button', { name: /submit/i }); - act(() => { - userEvent.click(submitButton); - }); - - expect(onSubmit).toHaveBeenCalledWith('Complete'); - }); - - it('will not submit if all sessions and pages are complete and user is not owner', async () => { - fetchMock.getOnce(sessionsUrl, [ - { id: 2, eventId: 1, data: { sessionName: 'Toothbrushing vol 2', status: 'Complete' } }, - { id: 3, eventId: 1, data: { sessionName: 'Toothbrushing vol 3', status: 'Complete' } }, - ]); - - act(() => { - render(); - }); - - // combobox isn't even present - const statusSelect = screen.queryByRole('combobox', { name: /status/i }); - expect(statusSelect).toBeNull(); - - // the status will be displayed as read only - const statusLabel = await screen.findByText('Event status'); - expect(statusLabel).toBeInTheDocument(); - const status = await screen.findByText('In progress'); - expect(status).toBeInTheDocument(); - - // and the submit button is disabled - const submitButton = screen.queryByRole('button', { name: /submit/i }); - expect(submitButton).toBeNull(); - }); - - it('sets a default status of not started if there is no form status and there are no sessions', async () => { - fetchMock.getOnce(sessionsUrl, []); - - act(() => { - render(); - }); - - const statusLabel = await screen.findByText('Event status'); - expect(statusLabel).toBeInTheDocument(); - const status = await screen.findByText('Not started'); - expect(status).toBeInTheDocument(); - - // there should be no select - const statusSelect = screen.queryByRole('combobox', { name: /status/i }); - expect(statusSelect).toBeNull(); - }); - }); -}); diff --git a/frontend/src/pages/TrainingReportForm/pages/__tests__/eventSummary.js b/frontend/src/pages/TrainingReportForm/pages/__tests__/eventSummary.js index adffa4d9cb..ada14fb7c4 100644 --- a/frontend/src/pages/TrainingReportForm/pages/__tests__/eventSummary.js +++ b/frontend/src/pages/TrainingReportForm/pages/__tests__/eventSummary.js @@ -10,7 +10,7 @@ import { useForm, FormProvider } from 'react-hook-form'; import userEvent from '@testing-library/user-event'; import selectEvent from 'react-select-event'; import { SCOPE_IDS } from '@ttahub/common'; -import eventSummary, { isPageComplete } from '../eventSummary'; +import EventSummary from '../eventSummary'; import NetworkContext from '../../../../NetworkContext'; import UserContext from '../../../../UserContext'; @@ -25,37 +25,22 @@ const defaultUser = { }; describe('eventSummary', () => { - describe('isPageComplete', () => { - it('returns true if form state is valid', () => { - expect(isPageComplete({ - getValues: jest.fn(() => ({ - pocIds: [1], - collaboratorIds: [1], - reasons: [1], - targetPopulations: [1], - })), - })).toBe(true); - }); - - it('returns false otherwise', () => { - expect(isPageComplete({ getValues: jest.fn(() => false) })).toBe(false); - }); - }); - describe('review', () => { - it('renders correctly', async () => { - act(() => { - render(<>{eventSummary.reviewSection()}); - }); - - expect(await screen.findByRole('heading', { name: /event summary/i })).toBeInTheDocument(); - }); - }); describe('render', () => { const onSaveDraft = jest.fn(); const defaultFormValues = { eventId: 'Event-id-1', eventName: 'Event-name-1', + ownerName: 'Owner-name-1', + pocIds: [1], + reasons: ['Complaint'], + targetPopulations: ['target population1', 'target population2'], + vision: 'This is a sample vision.', + eventIntendedAudience: 'recipient', + eventOrganizer: 'Sample organizer', + owner: { + name: 'Owner-name-1', + }, }; const RenderEventSummary = (user = defaultUser) => { @@ -64,42 +49,45 @@ describe('eventSummary', () => { defaultValues: defaultFormValues, }); + const additionalData = { + users: { + pointOfContact: [{ + id: 1, + fullName: 'Ted User', + nameWithNationalCenters: 'Ted User', + }], + collaborators: [ + { + id: 2, + fullName: 'Tedwina User', + nameWithNationalCenters: 'Tedwina User', + }, + ], + creators: [ + { id: 1, name: 'IST 1' }, + { id: 2, name: 'IST 2' }, + ], + }, + }; + + // set the hook form data that is returned from getValues(). + hookForm.getValues = () => defaultFormValues; + return ( - {eventSummary.render( - { - users: { - pointOfContact: [{ - id: 1, - fullName: 'Ted User', - nameWithNationalCenters: 'Ted User', - }], - collaborators: [ - { - id: 2, - fullName: 'Tedwina User', - nameWithNationalCenters: 'Tedwina User', - }, - ], - creators: [ - { id: 1, name: 'IST 1' }, - { id: 2, name: 'IST 2' }, - ], - }, - }, - defaultFormValues, - 1, - false, - jest.fn(), - onSaveDraft, - jest.fn(), - false, - 'key', - jest.fn(), - () => <>, - )} + <>} + /> @@ -107,8 +95,14 @@ describe('eventSummary', () => { }; it('renders event summary', async () => { + const adminUser = { + ...defaultUser, + permissions: [ + { regionId: 1, scopeId: ADMIN }, + ], + }; act(() => { - render(); + render(); }); const selections = document.querySelectorAll('button, input, textarea, select, a'); @@ -137,7 +131,7 @@ describe('eventSummary', () => { expect(onSaveDraft).toHaveBeenCalled(); }); - it('admin users can edit title and owner fields', async () => { + it('admin users can edit all fields', async () => { const adminUser = { ...defaultUser, permissions: [ @@ -148,34 +142,56 @@ describe('eventSummary', () => { render(); }); - // Event name. - const eventName = await screen.findByRole('textbox', { name: /event name required/i }); - expect(eventName).toBeInTheDocument(); + // Event ID. + expect(await screen.findByRole('textbox', { name: /event id required/i })).toBeInTheDocument(); + + // Event Name. + expect(await screen.findByRole('textbox', { name: /event name required/i })).toBeInTheDocument(); + + // Event creator. + expect(await screen.findByTestId('creator-select')).toBeInTheDocument(); + + // Event Organizer. + expect(await screen.findByRole('combobox', { name: /event organizer/i })).toBeInTheDocument(); + + // Event Collaborator. + expect(await screen.findByRole('combobox', { name: /event collaborators required select\.\.\./i })).toBeInTheDocument(); - // Change the value in the event name field. - const creatorSelect = await screen.findByTestId('creator-select'); - expect(creatorSelect).toBeInTheDocument(); + // Event Point of Contact. + expect(await screen.findByRole('combobox', { name: /event region point of contact/i })).toBeInTheDocument(); - // Update event name field. - userEvent.clear(eventName); - userEvent.type(eventName, 'Event name 2'); + // Event Intended Audience. + expect(await screen.findByRole('group', { name: /event intended audience required/i })).toBeInTheDocument(); - // Assert new event name. - expect(eventName).toHaveValue('Event name 2'); + // Event Training Type. + expect(await screen.findByRole('combobox', { name: /training type/i })).toBeInTheDocument(); + + // Event Reason. + expect(await screen.findByRole('combobox', { name: /reasons required complaint/i })).toBeInTheDocument(); + + // Event Target Population. + expect(await screen.findByRole('combobox', { name: /target populations addressed required target population1 target population2/i })).toBeInTheDocument(); + + // Event Vision. + expect(await screen.findByRole('textbox', { name: /event vision required/i })).toBeInTheDocument(); }); - it('non admin users cant edit title and owner fields', async () => { - const adminUser = { + it('non admin users cant edit certain fields', async () => { + const nonAdminUser = { ...defaultUser, permissions: [ { regionId: 1, scopeId: READ_WRITE_TRAINING_REPORTS }, ], }; act(() => { - render(); + render(); }); - expect(screen.queryAllByRole('textbox', { name: /event name required/i }).length).toBe(0); - expect(screen.queryAllByTestId('creator-select').length).toBe(0); + + // Event Collaborator. + expect(await screen.findByRole('combobox', { name: /event collaborators required select\.\.\./i })).toBeInTheDocument(); + + // Nine additional read only fields. + expect(screen.queryAllByTestId('read-only-label').length).toBe(9); }); }); }); diff --git a/frontend/src/pages/TrainingReportForm/pages/__tests__/visionGoal.js b/frontend/src/pages/TrainingReportForm/pages/__tests__/visionGoal.js deleted file mode 100644 index 1fe1818793..0000000000 --- a/frontend/src/pages/TrainingReportForm/pages/__tests__/visionGoal.js +++ /dev/null @@ -1,199 +0,0 @@ -/* eslint-disable react/jsx-props-no-spreading */ -import React from 'react'; -import moment from 'moment'; -import fetchMock from 'fetch-mock'; -import { render, screen, act } from '@testing-library/react'; -import { useForm, FormProvider } from 'react-hook-form'; -import userEvent from '@testing-library/user-event'; -import vision, { isPageComplete } from '../vision'; -import NetworkContext from '../../../../NetworkContext'; -import UserContext from '../../../../UserContext'; -import AppLoadingContext from '../../../../AppLoadingContext'; - -describe('vision', () => { - describe('isPageComplete', () => { - it('returns true if form state is valid', () => { - expect(isPageComplete({ getValues: jest.fn(() => true) })).toBe(true); - }); - - it('returns false otherwise', () => { - expect(isPageComplete({ getValues: jest.fn(() => false) })).toBe(false); - }); - }); - describe('review', () => { - it('renders correctly', async () => { - act(() => { - render(<>{vision.reviewSection()}); - }); - - expect(await screen.findByRole('heading', { name: /vision/i })).toBeInTheDocument(); - }); - }); - describe('render', () => { - afterEach(() => fetchMock.restore()); - - const onSaveDraft = jest.fn(); - const userId = 1; - const todaysDate = moment().format('YYYY-MM-DD'); - - const defaultFormValues = { - vision: 'test vision', - eventId: 'R01-TR-23-1111', - }; - - const defaultUser = { user: { id: userId, roles: [] } }; - - // eslint-disable-next-line react/prop-types - const RenderVision = ({ formValues = defaultFormValues, user = defaultUser }) => { - const hookForm = useForm({ - mode: 'onBlur', - defaultValues: formValues, - }); - - return ( - - - - - {vision.render( - { - users: { - pointOfContact: [{ - id: 1, - fullName: 'Ted User', - }], - collaborators: [ - { - id: 2, - fullName: 'Tedwina User', - }, - ], - }, - }, - formValues, - 1, - false, - jest.fn(), - onSaveDraft, - jest.fn(), - false, - 'key', - jest.fn(), - () => <>, - )} - - - - - ); - }; - - it('renders vision', async () => { - fetchMock.get('/api/session-reports/eventId/1111', []); - act(() => { - render(); - }); - - const visionInput = await screen.findByLabelText(/vision/i); - expect(visionInput).toHaveValue('test vision'); - - userEvent.clear(visionInput); - userEvent.type(visionInput, 'new vision'); - - const saveDraftButton = await screen.findByRole('button', { name: /save draft/i }); - userEvent.click(saveDraftButton); - expect(onSaveDraft).toHaveBeenCalled(); - }); - - it('renders checkbox for POC', async () => { - fetchMock.get('/api/session-reports/eventId/1111', []); - const updatedValues = { - ...defaultFormValues, - pocIds: [userId], - pocComplete: false, - }; - act(() => { - render(); - }); - - const visionInput = await screen.findByLabelText(/vision/i); - expect(visionInput).toHaveValue('test vision'); - - userEvent.clear(visionInput); - userEvent.type(visionInput, 'new vision'); - const checkbox = await screen.findByLabelText(/Email the event creator and collaborator to let them know my work is complete/i); - expect(checkbox).not.toBeChecked(); - - act(() => { - userEvent.click(checkbox); - }); - - expect(checkbox).toBeChecked(); - - const hiddenInputs = document.querySelectorAll('input[type="hidden"]'); - expect(hiddenInputs.length).toBe(2); - - const hiddenInputValues = Array.from(hiddenInputs).map((input) => input.value); - expect(hiddenInputValues.includes(todaysDate)).toBe(true); - expect(hiddenInputValues.includes(userId.toString())).toBe(true); - - const saveDraftButton = await screen.findByRole('button', { name: /save draft/i }); - userEvent.click(saveDraftButton); - expect(onSaveDraft).toHaveBeenCalled(); - }); - - it('hides checkbox for POC with incorrect roles', async () => { - fetchMock.get('/api/session-reports/eventId/1111', []); - const updatedValues = { - ...defaultFormValues, - pocIds: [userId], - pocComplete: false, - }; - act(() => { - render(); - }); - - const visionInput = await screen.findByLabelText(/vision/i); - expect(visionInput).toHaveValue('test vision'); - - userEvent.clear(visionInput); - userEvent.type(visionInput, 'new vision'); - expect(await screen.queryAllByText(/Email the event creator and collaborator to let them know my work is complete/i).length).toBe(0); - - const hiddenInputs = document.querySelectorAll('input[type="hidden"]'); - expect(hiddenInputs.length).toBe(3); - - const saveDraftButton = await screen.findByRole('button', { name: /save draft/i }); - userEvent.click(saveDraftButton); - expect(onSaveDraft).toHaveBeenCalled(); - }); - - it('shows read only data', async () => { - fetchMock.get('/api/session-reports/eventId/1111', []); - const updatedValues = { - ...defaultFormValues, - pocIds: [userId], - pocComplete: true, - pocCompleteId: userId, - pocCompleteDate: todaysDate, - vision: 'incredible new vision', - }; - act(() => { - render(); - }); - - // confirm alert - const alert = await screen.findByText(/sent an email to the event creator and collaborator/i); - expect(alert).toBeVisible(); - - // confirm read-only - const checkbox = screen.queryByRole('checkbox'); - expect(checkbox).not.toBeInTheDocument(); - const textareas = document.querySelectorAll('textarea'); - expect(textareas.length).toBe(0); - - const vis = await screen.findByText('incredible new vision'); - expect(vis).toBeVisible(); - }); - }); -}); diff --git a/frontend/src/pages/TrainingReportForm/pages/completeEvent.js b/frontend/src/pages/TrainingReportForm/pages/completeEvent.js deleted file mode 100644 index ee38d6917b..0000000000 --- a/frontend/src/pages/TrainingReportForm/pages/completeEvent.js +++ /dev/null @@ -1,345 +0,0 @@ -import React, { useState, useContext, useEffect } from 'react'; -import { TRAINING_REPORT_STATUSES } from '@ttahub/common'; -import { useFormContext } from 'react-hook-form'; -import { ErrorMessage as ReactHookFormError } from '@hookform/error-message'; -import PropTypes from 'prop-types'; -import { Helmet } from 'react-helmet'; -import { useHistory } from 'react-router-dom'; -import { - Alert, Button, Table, Dropdown, ErrorMessage, -} from '@trussworks/react-uswds'; -import FormItem from '../../../components/FormItem'; -import AppLoadingContext from '../../../AppLoadingContext'; -import UserContext from '../../../UserContext'; -import IndicatesRequiredField from '../../../components/IndicatesRequiredField'; -import { sessionsByEventId } from '../../../fetchers/event'; -import ReadOnlyField from '../../../components/ReadOnlyField'; -import { InProgress, Closed } from '../../../components/icons'; -import { getEventIdSlug } from '../constants'; - -const pages = { - 1: 'Event summary', - 2: 'Vision and goal', -}; - -const position = 3; -const path = 'complete-event'; - -const sessionStatusIcons = { - 'In progress': , - Complete: , -}; - -const CompleteEvent = ({ - onSubmit, - formData, - onSaveForm, - onUpdatePage, - DraftAlert, -}) => { - const { setError, clearErrors, formState } = useFormContext(); - const { user } = useContext(UserContext); - const { isAppLoading, setIsAppLoading } = useContext(AppLoadingContext); - const [error, updateError] = useState(); - const [sessions, setSessions] = useState(); - const [showSubmissionError, setShowSubmissionError] = useState(false); - const [showError, setShowError] = useState(false); - - const history = useHistory(); - - // we store this in state and not the form data because we don't want to - // automatically update the form object when the user changes the status dropdown - // we need to validate before saving, and we only want the status to change when the - // form is explicitly submitted - const [updatedStatus, setUpdatedStatus] = useState(formData.status || 'Not started'); - - const { errors } = formState; - const isOwner = user && user.id === formData.ownerId; - - useEffect(() => { - // we want to set the status to in progress if the user adds a session - // and the status was previously not started - if (sessions && sessions.length && updatedStatus === 'Not started') { - setUpdatedStatus('In progress'); - } - }, [sessions, updatedStatus]); - - useEffect(() => { - async function getSessions() { - try { - setIsAppLoading(true); - const res = await sessionsByEventId(getEventIdSlug(formData.eventId)); - setSessions(res); - } catch (e) { - updateError('Unable to load sessions'); - setSessions([]); - } finally { - setIsAppLoading(false); - } - } - - if (!sessions && formData.eventId) { - getSessions(); - } - }, [formData.eventId, sessions, setIsAppLoading]); - - useEffect(() => { - if (errors.status && !showError && ((sessions && sessions.length === 0) || !isOwner)) { - /** - * adding this to clear the error message after 10 seconds - * this only shows & clears in the case where the "read only" status field is shown - * - */ - setShowError(true); - setTimeout(() => { - clearErrors('status'); - setShowError(false); - }, 10000); - } - - return () => { - clearTimeout(); - }; - }, [clearErrors, errors.status, isOwner, sessions, showError]); - - const areAllSessionsComplete = sessions && sessions.length && sessions.every((session) => session.data.status === 'Complete'); - const incompleteSessions = !sessions || areAllSessionsComplete ? [] : sessions.filter((session) => session.data.status !== 'Complete'); - - const incompletePages = (() => Object.keys(pages) - .filter((key) => formData.pageState[key] !== TRAINING_REPORT_STATUSES.COMPLETE) - .map((key) => pages[key]))(); - - const areAllPagesComplete = incompletePages.length === 0; - - const onFormSubmit = async () => { - if (updatedStatus !== 'Complete') { - setError('status', { message: 'Event must be complete to submit' }); - return; - } - - if (!areAllSessionsComplete || !areAllPagesComplete) { - setShowSubmissionError(true); - return; - } - - await onSubmit(updatedStatus); - }; - - if (!sessions) { - return null; - } - - let options = [ - , - , - , - ]; - - if (!sessions.length) { - options = [ - , - , - ]; - } - - const SubmitButton = () => { - const onSuspend = async () => { - await onSaveForm(updatedStatus); - const newPath = '/training-reports/suspended'; - history.push(newPath); - }; - - if (isOwner && updatedStatus === 'Suspended') { - return (); - } - - if (isOwner) { - return (); - } - - return null; - }; - - return ( -
    - - Complete Event - - - -

    Review the information in each section before submitting. Once submitted, the report will no longer be editable.

    - {error && ( -
    - - {error} - -
    - )} - - - {sessions.length} - - - { (!isOwner) && ( - <> - - {updatedStatus} - - {message}} - /> - - )} - - {sessions.length > 0 && ( - <> -

    Session status

    - - - - - - - - - {sessions.map((session) => ( - - - - - ))} - -
    Session nameSession status
    - {session.data.sessionName} - - {sessionStatusIcons[session.data.status]} - {session.data.status} -
    - - )} - - { isOwner && ( -
    - - { - clearErrors('status'); - setUpdatedStatus(e.target.value); - }} - > - {options} - - -
    - )} - - {showSubmissionError && ( -
    - -

    Incomplete report

    - { - !areAllPagesComplete && ( - <> -

    This report cannot be submitted until all sections are complete. Please review the following sections:

    -
      - {incompletePages.map((page) => ( -
    • - {page} -
    • - ))} -
    - - ) - } - { - !areAllSessionsComplete && ( - <> -

    This report cannot be submitted until all sessions are complete.

    -
      - {incompleteSessions.map((session) => ( -
    • - {session.data.sessionName} -
    • - ))} -
    - - ) - } -
    -
    - )} - - -
    - - - -
    - -
    - ); -}; - -CompleteEvent.propTypes = { - formData: PropTypes.shape({ - id: PropTypes.number, - eventId: PropTypes.string, - status: PropTypes.string, - pageState: PropTypes.shape({ - 1: PropTypes.string, - 2: PropTypes.string, - }), - ownerId: PropTypes.number, - }), - onSaveForm: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onUpdatePage: PropTypes.func.isRequired, - DraftAlert: PropTypes.node.isRequired, -}; - -CompleteEvent.defaultProps = { - formData: {}, -}; - -export default { - position, - review: false, - label: 'Complete event', - path, - isPageComplete: ({ getValues }) => { - const { status } = getValues(); - return status === TRAINING_REPORT_STATUSES.COMPLETE; - }, - render: - ( - _additionalData, - formData, - _reportId, - _isAppLoading, - _onContinue, - onSaveForm, - onUpdatePage, - _weAreAutoSaving, - _datePickerKey, - onSubmit, - DraftAlert, - ) => ( - - ), -}; diff --git a/frontend/src/pages/TrainingReportForm/pages/eventSummary.js b/frontend/src/pages/TrainingReportForm/pages/eventSummary.js index 6ed0875143..6e46f728b4 100644 --- a/frontend/src/pages/TrainingReportForm/pages/eventSummary.js +++ b/frontend/src/pages/TrainingReportForm/pages/eventSummary.js @@ -11,6 +11,7 @@ import { REASONS, TRAINING_REPORT_STATUSES, } from '@ttahub/common'; + import { useFormContext, Controller } from 'react-hook-form'; import { Label, @@ -18,19 +19,16 @@ import { Fieldset, Radio, TextInput, + Button, + Textarea, } from '@trussworks/react-uswds'; import MultiSelect from '../../../components/MultiSelect'; import FormItem from '../../../components/FormItem'; import IndicatesRequiredField from '../../../components/IndicatesRequiredField'; -import NavigatorButtons from '../../../components/Navigator/components/NavigatorButtons'; import ReadOnlyField from '../../../components/ReadOnlyField'; import selectOptionsReset from '../../../components/selectOptionsReset'; import ControlledDatePicker from '../../../components/ControlledDatePicker'; import Req from '../../../components/Req'; -import { - eventSummaryFields, - pageComplete, -} from '../constants'; import UserContext from '../../../UserContext'; import isAdmin from '../../../permissions'; @@ -56,7 +54,13 @@ const eventOrganizerOptions = [ 'IST TTA/Visit', ].map((option) => ({ value: option, label: option })); -const EventSummary = ({ additionalData, datePickerKey }) => { +const EventSummary = ({ + additionalData, + datePickerKey, + isAppLoading, + showSubmitModal, + onSaveDraft, +}) => { const { register, control, @@ -82,7 +86,6 @@ const EventSummary = ({ additionalData, datePickerKey }) => { }; const { - eventId, eventName, owner, status, @@ -92,62 +95,205 @@ const EventSummary = ({ additionalData, datePickerKey }) => { const hasAdminRights = isAdmin(user); const { users: { collaborators, pointOfContact, creators } } = additionalData; - + const adminCanEdit = hasAdminRights && (status !== TRAINING_REPORT_STATUSES.COMPLETE); const ownerName = owner && owner.name ? owner.name : ''; - return ( -
    - - Event Summary - - + const getIntendedAudience = (value) => { + let audience = ''; + if (value) { + audience = data.eventIntendedAudience.charAt(0).toUpperCase() + + data.eventIntendedAudience.slice(1); + } + return audience; + }; - - {eventId} - + const getPointOfContacts = (pocs) => { + let pocsToDisplay = []; + if (pocs && pocs.length) { + pocsToDisplay = pointOfContact.filter( + (poc) => pocs.includes(poc.id), + ).map((poc) => poc.fullName); + } + return pocsToDisplay.join(', '); + }; - {hasAdminRights && (status !== TRAINING_REPORT_STATUSES.COMPLETE) ? ( + const getReadOnlyReasons = (reasons) => { + if (!reasons || reasons.length === 0) { + return ''; + } + return reasons.join(', '); + }; - <> -
    - - { + if (!tvalue || tvalue.length === 0) { + return ''; + } + return tvalue.join(', '); + }; + + return ( +
    +
    + + Event Summary + +
    +

    Event summary

    +
    + + {adminCanEdit ? ( + <> +
    + + + +
    +
    + + + +
    +
    + + ( + option.value === value)} + inputId="eventOrganizer" + name="eventOrganizer" + className="usa-select" + styles={selectOptionsReset} + components={{ + DropdownIndicator: null, + }} + onChange={(s) => { + controllerOnChange(s.value); + }} + inputRef={register({ required: 'Select an event organizer' })} + options={eventOrganizerOptions} + required + /> + )} + control={control} + rules={{ + validate: (value) => { + if (!value || value.length === 0) { + return 'Select an event organizer'; + } + return true; + }, + }} + name="eventOrganizer" + defaultValue="" + /> +
    + + ) + : ( + <> + + {eventName} + + + {ownerName} + + + {data.eventOrganizer} + + + )} + +
    + ( + render={({ onChange: controllerOnChange, value }) => ( ( + value.includes(option.id) + ))} + inputId="pocIds" + name="pocIds" + className="usa-select" + styles={selectOptionsReset} + components={{ + DropdownIndicator: null, + }} + onChange={(s) => { + controllerOnChange(s.map((option) => option.id)); + }} + inputRef={register({ required: 'Select at least one event region point of contact' })} + getOptionLabel={(option) => option.fullName} + getOptionValue={(option) => option.id} + options={pointOfContact} + required + isMulti + /> + )} + control={control} + rules={{ + validate: (value) => { + if (!value || value.length === 0) { + return 'Select at least one event region point of contact'; + } + return true; + }, + }} + name="pocIds" + defaultValue={[]} + /> +
    +
    +
    + + + + +
    +
    + + ) : ( + <> + + {getPointOfContacts(data.pocIds)} - - {ownerName} + + {getIntendedAudience(data.eventIntendedAudience)} )} -
    - - ( - ( - value.includes(collaborator.id) - ))} - inputId="collaboratorIds" - name="collaboratorIds" - className="usa-select" - styles={selectOptionsReset} - components={{ - DropdownIndicator: null, - }} - onChange={(s) => { - controllerOnChange(s.map((option) => option.id)); - }} - inputRef={register({ required: 'Select at least one collaborator' })} - getOptionLabel={(option) => option.nameWithNationalCenters} - getOptionValue={(option) => option.id} - options={collaborators} - required - /> - )} - control={control} - rules={{ - validate: (value) => { - if (!value || value.length === 0) { - return 'Select at least one collaborator'; - } - return true; - }, - }} - name="collaboratorIds" - defaultValue={[]} - /> - - -
    - -
    - - ( -