diff --git a/docker-compose.yml b/docker-compose.yml index f366b61f31..e48af8837c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,9 @@ services: aws-cli: image: amazon/aws-cli env_file: .env - command: ["--endpoint-url", "$S3_ENDPOINT", "s3api", "create-bucket", "--bucket", "$S3_BUCKET"] + command: ["--endpoint-url", "http://minio:9000", "s3api", "create-bucket", "--bucket", "$S3_BUCKET"] + depends_on: + - minio clamav-rest: image: ajilaag/clamav-rest ports: diff --git a/frontend/src/App.css b/frontend/src/App.css index 82f6a408b0..2ffa03179d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -73,6 +73,9 @@ body { } @media print { + .smart-hub-header { + position: relative; + } * { color: #000 !important; diff --git a/frontend/src/components/FileUploader.js b/frontend/src/components/FileUploader.js index 9206935ce1..0ce8a61435 100644 --- a/frontend/src/components/FileUploader.js +++ b/frontend/src/components/FileUploader.js @@ -33,7 +33,7 @@ export const upload = async (file, reportId, attachmentType, setErrorMessage) => } setErrorMessage(null); return { - id: res.id, originalFileName: file.name, fileSize: file.size, status: 'UPLOADED', + id: res.id, originalFileName: file.name, fileSize: file.size, status: 'UPLOADED', url: res.url, }; }; diff --git a/frontend/src/components/Navigator/components/__tests__/SideNav.js b/frontend/src/components/Navigator/components/__tests__/SideNav.js index ae84297d00..051beb07c1 100644 --- a/frontend/src/components/Navigator/components/__tests__/SideNav.js +++ b/frontend/src/components/Navigator/components/__tests__/SideNav.js @@ -13,7 +13,7 @@ import { import { REPORT_STATUSES } from '../../../../Constants'; describe('SideNav', () => { - const renderNav = (state, onNavigation = () => {}, current = false) => { + const renderNav = (state, onNavigation = () => {}, current = false, errorMessage = null) => { const pages = [ { label: 'test', @@ -34,6 +34,7 @@ describe('SideNav', () => { pages={pages} skipTo="skip" skipToMessage="message" + errorMessage={errorMessage} />, ); }; @@ -60,6 +61,20 @@ describe('SideNav', () => { expect(complete).toBeVisible(); }); + it('approved', () => { + renderNav(REPORT_STATUSES.APPROVED); + const complete = screen.getByText('Approved'); + expect(complete).toHaveClass('smart-hub--tag-submitted'); + expect(complete).toBeVisible(); + }); + + it('needs action', () => { + renderNav(REPORT_STATUSES.NEEDS_ACTION); + const complete = screen.getByText('Needs Action'); + expect(complete).toHaveClass('smart-hub--tag-needs-action'); + expect(complete).toBeVisible(); + }); + it('submitted', () => { renderNav(REPORT_STATUSES.SUBMITTED); const submitted = screen.getByText('Submitted'); @@ -68,6 +83,12 @@ describe('SideNav', () => { }); }); + it('displays error message', async () => { + renderNav(REPORT_STATUSES.SUBMITTED, () => {}, false, 'error'); + const alert = await screen.findByTestId('alert'); + expect(alert).toBeVisible(); + }); + it('clicking a nav item calls onNavigation', () => { const onNav = jest.fn(); renderNav(NOT_STARTED, onNav); diff --git a/frontend/src/components/Navigator/index.js b/frontend/src/components/Navigator/index.js index e3b0b34a36..f2cf10b0dc 100644 --- a/frontend/src/components/Navigator/index.js +++ b/frontend/src/components/Navigator/index.js @@ -52,6 +52,7 @@ function Navigator({ const hookForm = useForm({ mode: 'onChange', defaultValues: formData, + shouldUnregister: false, }); const { diff --git a/frontend/src/components/__tests__/FileUploader.js b/frontend/src/components/__tests__/FileUploader.js index 481f47d26f..961714bb06 100644 --- a/frontend/src/components/__tests__/FileUploader.js +++ b/frontend/src/components/__tests__/FileUploader.js @@ -32,10 +32,10 @@ describe('upload tests', () => { const mockFile = { name: 'MockFile', size: 2000 }; const mockSetErrorMessage = jest.fn(); it('can upload a file and return the correct information', async () => { - const mockFileUpload = jest.spyOn(fileFetcher, 'uploadFile').mockImplementation(async () => ({ id: 1 })); + const mockFileUpload = jest.spyOn(fileFetcher, 'uploadFile').mockImplementation(async () => ({ id: 1, url: 'url' })); const got = await upload(mockFile, 1, 'fakeAttachment', mockSetErrorMessage); expect(got).toStrictEqual({ - id: 1, originalFileName: mockFile.name, fileSize: mockFile.size, status: 'UPLOADED', + id: 1, originalFileName: mockFile.name, fileSize: mockFile.size, status: 'UPLOADED', url: 'url', }); expect(mockFileUpload).toHaveBeenCalled(); expect(mockSetErrorMessage).toHaveBeenCalledWith(null); diff --git a/frontend/src/fetchers/__tests__/activityReports.js b/frontend/src/fetchers/__tests__/activityReports.js new file mode 100644 index 0000000000..b9ae5bca27 --- /dev/null +++ b/frontend/src/fetchers/__tests__/activityReports.js @@ -0,0 +1,46 @@ +import join from 'url-join'; +import fetchMock from 'fetch-mock'; + +import { + submitReport, saveReport, reviewReport, resetToDraft, +} from '../activityReports'; + +describe('activityReports fetcher', () => { + afterEach(() => fetchMock.restore()); + + describe('submitReport', () => { + it('returns the report', async () => { + const report = { id: 1 }; + fetchMock.post(join('api', 'activity-reports', '1', 'submit'), report); + const savedReport = await submitReport(1, report); + expect(savedReport).toEqual(report); + }); + }); + + describe('saveReport', () => { + it('returns the report', async () => { + const report = { id: 1 }; + fetchMock.put(join('api', 'activity-reports', '1'), report); + const savedReport = await saveReport(1, report); + expect(savedReport).toEqual(report); + }); + }); + + describe('resetToDraft', () => { + it('calls reset and returns the result', async () => { + const report = { id: 1 }; + fetchMock.put(join('api', 'activity-reports', '1', 'reset'), report); + const savedReport = await resetToDraft(1); + expect(savedReport).toEqual(report); + }); + }); + + describe('reviewReport', () => { + it('returns the report', async () => { + const report = { id: 1 }; + fetchMock.put(join('api', 'activity-reports', '1', 'review'), report); + const savedReport = await reviewReport(1, report); + expect(savedReport).toEqual(report); + }); + }); +}); diff --git a/frontend/src/pages/ActivityReport/Pages/Review/FileReviewItem.js b/frontend/src/pages/ActivityReport/Pages/Review/FileReviewItem.js new file mode 100644 index 0000000000..1b1a591639 --- /dev/null +++ b/frontend/src/pages/ActivityReport/Pages/Review/FileReviewItem.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Grid } from '@trussworks/react-uswds'; + +const APPROVED = 'APPROVED'; + +const FileReviewItem = ({ filename, url, status }) => { + const approved = status === APPROVED; + return ( + + + {filename} + + +
+ {approved + && Download} + {!approved + &&
Not Approved
} +
+
+
+ ); +}; + +FileReviewItem.propTypes = { + filename: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, +}; + +export default FileReviewItem; diff --git a/frontend/src/pages/ActivityReport/Pages/Review/ReviewItem.js b/frontend/src/pages/ActivityReport/Pages/Review/ReviewItem.js new file mode 100644 index 0000000000..dc287d97f2 --- /dev/null +++ b/frontend/src/pages/ActivityReport/Pages/Review/ReviewItem.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { useFormContext } from 'react-hook-form'; + +const ReviewItem = ({ label, name, path }) => { + const { watch } = useFormContext(); + const value = watch(name); + let values = value; + + if (!Array.isArray(value)) { + values = [value]; + } + + if (path) { + values = values.map((v) => _.get(v, path)); + } + + const emptySelector = value && value.length > 0 ? '' : 'smart-hub-review-item--empty'; + const classes = ['margin-top-1', emptySelector].filter((x) => x !== '').join(' '); + + return ( +
+
+ {label} +
+
+ {values.map((v, index) => ( +
+ {Number.isNaN(v) ? '' : v} +
+ ))} +
+
+ ); +}; + +ReviewItem.propTypes = { + label: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + path: PropTypes.string, +}; + +ReviewItem.defaultProps = { + path: '', +}; + +export default ReviewItem; diff --git a/frontend/src/pages/ActivityReport/Pages/Review/ReviewPage.js b/frontend/src/pages/ActivityReport/Pages/Review/ReviewPage.js new file mode 100644 index 0000000000..0be343f134 --- /dev/null +++ b/frontend/src/pages/ActivityReport/Pages/Review/ReviewPage.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { some } from 'lodash'; +import { useFormContext } from 'react-hook-form'; + +import Section from './ReviewSection'; +import ReviewItem from './ReviewItem'; + +const ReviewPage = ({ sections, path }) => { + const { getValues } = useFormContext(); + return ( + <> + {sections.map((section) => { + const names = section.items.map((item) => item.name); + const values = getValues(names); + const isEmpty = !some(values, (value) => value && value.length); + return ( +
+ {section.items.map((item) => ( + + ))} +
+ ); + })} + + ); +}; + +ReviewPage.propTypes = { + path: PropTypes.string.isRequired, + sections: PropTypes.arrayOf(PropTypes.shape({ + title: PropTypes.string, + anchor: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string, + path: PropTypes.string, + name: PropTypes.string, + })), + })).isRequired, +}; + +export default ReviewPage; diff --git a/frontend/src/pages/ActivityReport/Pages/Review/ReviewSection.js b/frontend/src/pages/ActivityReport/Pages/Review/ReviewSection.js new file mode 100644 index 0000000000..bfb3412e5a --- /dev/null +++ b/frontend/src/pages/ActivityReport/Pages/Review/ReviewSection.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { HashLink } from 'react-router-hash-link'; + +const Section = ({ + title, children, basePath, anchor, hidePrint, +}) => { + const classes = [ + 'smart-hub-review-section', + 'margin-top-2 desktop:margin-top-0', + hidePrint ? 'smart-hub-review-section--empty no-print' : '', + 'margin-bottom-3', + ].filter((x) => x).join(' '); + + return ( +
+
+
+ {title} +
+
+ + Edit + +
+
+ {children} +
+ ); +}; + +Section.propTypes = { + hidePrint: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, + anchor: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + basePath: PropTypes.string.isRequired, +}; + +export default Section; diff --git a/frontend/src/pages/ActivityReport/Pages/Review/__tests__/FileReviewItem.js b/frontend/src/pages/ActivityReport/Pages/Review/__tests__/FileReviewItem.js new file mode 100644 index 0000000000..69b6ca2736 --- /dev/null +++ b/frontend/src/pages/ActivityReport/Pages/Review/__tests__/FileReviewItem.js @@ -0,0 +1,35 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; + +import FileReviewItem from '../FileReviewItem'; + +// eslint-disable-next-line react/prop-types +const RenderFileReviewItem = ({ status = 'APPROVED' }) => ( + +); + +describe('ReviewPage', () => { + it('displays the filename', async () => { + render(); + const item = await screen.findByText('filename'); + expect(item).toBeVisible(); + }); + + it('displays the download link', async () => { + render(); + const link = await screen.findByRole('link'); + expect(link).toHaveAttribute('href', 'http://localhost:3000'); + }); + + it('displays "not approved" if the file has not been approved', async () => { + render(); + const status = await screen.findByText('Not Approved'); + expect(status).toBeVisible(); + }); +}); diff --git a/frontend/src/pages/ActivityReport/Pages/Review/__tests__/reviewItem.js b/frontend/src/pages/ActivityReport/Pages/Review/__tests__/ReviewPage.js similarity index 77% rename from frontend/src/pages/ActivityReport/Pages/Review/__tests__/reviewItem.js rename to frontend/src/pages/ActivityReport/Pages/Review/__tests__/ReviewPage.js index f7279c8c9f..cbc0182181 100644 --- a/frontend/src/pages/ActivityReport/Pages/Review/__tests__/reviewItem.js +++ b/frontend/src/pages/ActivityReport/Pages/Review/__tests__/ReviewPage.js @@ -1,8 +1,10 @@ +/* eslint-disable react/jsx-props-no-spreading */ import React from 'react'; import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; -import accordionItem from '../reviewItem'; +import { FormProvider, useForm } from 'react-hook-form'; +import ReviewPage from '../ReviewPage'; const sections = [ { @@ -37,13 +39,28 @@ const values = { object: { test: 'test' }, }; -describe('AccordionItem', () => { +const RenderReviewPage = () => { + const hookForm = useForm({ + mode: 'onChange', + defaultValues: values, + shouldUnregister: false, + }); + return ( + + + + + + ); +}; + +describe('ReviewPage', () => { beforeEach(() => { - const item = accordionItem('id', 'title', sections, values); render( - - {item.content} - , + , ); }); diff --git a/frontend/src/pages/ActivityReport/Pages/Review/reviewItem.js b/frontend/src/pages/ActivityReport/Pages/Review/reviewItem.js deleted file mode 100644 index 40ad70907a..0000000000 --- a/frontend/src/pages/ActivityReport/Pages/Review/reviewItem.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - Review items for activity report review section - - These items are for use with the USWDS accordion component. This module is split in the - following way: - - 1. Review Item: this represents a page in the activity report, and contains multiple - review sections. - 2. Sections: this represents a "fieldset" in the report, or the major sections within a page - of the activity report, they contain items. - 3. Items: these are the individual form fields for the activity report -*/ -import PropTypes from 'prop-types'; -import { HashLink } from 'react-router-hash-link'; -import _ from 'lodash'; -import React from 'react'; - -const itemType = { - label: PropTypes.string.isRequired, - path: PropTypes.string, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.shape({}), - PropTypes.arrayOf(PropTypes.string), - PropTypes.arrayOf(PropTypes.shape({})), - ]), -}; - -const sectionType = { - title: PropTypes.string.isRequired, - anchor: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.shape(itemType)).isRequired, - basePath: PropTypes.string.isRequired, -}; - -const Section = ({ - title, items, basePath, anchor, -}) => { - const isEmpty = !items.some(({ value }) => value && value.length); - const classes = [ - 'smart-hub-review-section', - 'margin-top-2 desktop:margin-top-0', - isEmpty ? 'smart-hub-review-section--empty no-print' : '', - 'margin-bottom-3', - ].filter((x) => x).join(' '); - - return ( -
-
-
- {title} -
-
- - Edit - -
-
- {items.map(({ label, value, path }) => ( - - ))} -
- ); -}; - -Section.propTypes = sectionType; - -const Item = ({ label, value, path }) => { - let values = value; - - if (!Array.isArray(value)) { - values = [value]; - } - - if (path) { - values = values.map((v) => _.get(v, path)); - } - - const emptySelector = value && value.length > 0 ? '' : 'smart-hub-review-item--empty'; - const classes = ['margin-top-1', emptySelector].filter((x) => x !== '').join(' '); - - return ( -
-
- {label} -
-
- {values.map((v, index) => ( -
- {Number.isNaN(v) ? '' : v} -
- ))} -
-
- ); -}; - -Item.propTypes = itemType; - -Item.defaultProps = { - path: '', - value: null, -}; - -const ReviewItem = ({ formData, sections, basePath }) => ( - <> - {sections.map((section) => ( -
{ - const { path, label } = item; - const value = _.get(formData, item.name, ''); - return { - label, - value, - path, - }; - })} - /> - ))} - -); - -ReviewItem.propTypes = { - formData: PropTypes.shape({}).isRequired, - basePath: PropTypes.string.isRequired, - sections: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.string.isRequired, - anchor: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.shape(itemType)).isRequired, - }), - ).isRequired, -}; - -/* - This is the format the USWDS accordion expects of accordion items -*/ -export default (id, title, sections, formData) => ({ - id, - title, - content: , -}); diff --git a/frontend/src/pages/ActivityReport/Pages/__tests__/topicsResources.js b/frontend/src/pages/ActivityReport/Pages/__tests__/topicsResources.js new file mode 100644 index 0000000000..8a965dc01b --- /dev/null +++ b/frontend/src/pages/ActivityReport/Pages/__tests__/topicsResources.js @@ -0,0 +1,45 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +import topics from '../topicsResources'; + +// eslint-disable-next-line react/prop-types +const RenderTopicsResourcesReview = ({ data }) => { + const history = createMemoryHistory(); + const hookForm = useForm({ + mode: 'onChange', + defaultValues: data, + }); + // eslint-disable-next-line react/prop-types + return ( + + + {topics.reviewSection()} + + + ); +}; + +describe('Topics & resources', () => { + const data = { + otherResources: [{ originalFileName: 'other', url: { url: 'http://localhost/other' }, status: 'APPROVED' }], + attachments: [{ originalFileName: 'attachment', url: { url: 'http://localhost/attachment' }, status: 'APPROVED' }], + resourcesUsed: 'resources', + topics: 'topics', + }; + + describe('review page', () => { + it('displays attachments and other resources', async () => { + render(); + expect(await screen.findByText('other')).toBeVisible(); + expect(await screen.findByText('attachment')).toBeVisible(); + expect(await screen.findByText('resources')).toBeVisible(); + expect(await screen.findByText('topics')).toBeVisible(); + }); + }); +}); diff --git a/frontend/src/pages/ActivityReport/Pages/activitySummary.js b/frontend/src/pages/ActivityReport/Pages/activitySummary.js index 91f2fb4dce..337d501624 100644 --- a/frontend/src/pages/ActivityReport/Pages/activitySummary.js +++ b/frontend/src/pages/ActivityReport/Pages/activitySummary.js @@ -8,6 +8,7 @@ import { Fieldset, Radio, Grid, TextInput, Checkbox, } from '@trussworks/react-uswds'; +import ReviewPage from './Review/ReviewPage'; import DatePicker from '../../../components/DatePicker'; import MultiSelect from '../../../components/MultiSelect'; import { @@ -433,11 +434,15 @@ const sections = [ }, ]; +const ReviewSection = () => ( + +); + export default { position: 1, label: 'Activity summary', path: 'activity-summary', - sections, + reviewSection: () => , review: false, render: (additionalData) => { const { recipients, collaborators } = additionalData; diff --git a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js index 0c948a064a..3e7e9d10da 100644 --- a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js +++ b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js @@ -6,6 +6,7 @@ import { } from '@trussworks/react-uswds'; import useDeepCompareEffect from 'use-deep-compare-effect'; import { useFormContext } from 'react-hook-form'; +import ReviewPage from './Review/ReviewPage'; import GoalPicker from './components/GoalPicker'; import { getGoals } from '../../../fetchers/activityReports'; @@ -83,12 +84,16 @@ const sections = [ }, ]; +const ReviewSection = () => ( + +); + export default { position: 3, label: 'Goals and objectives', path: 'goals-objectives', review: false, - sections, + reviewSection: () => , render: (additionalData, formData) => { const recipients = formData.activityRecipients || []; const { activityRecipientType } = formData; diff --git a/frontend/src/pages/ActivityReport/Pages/index.js b/frontend/src/pages/ActivityReport/Pages/index.js index dbec25836e..005ffd9e5c 100644 --- a/frontend/src/pages/ActivityReport/Pages/index.js +++ b/frontend/src/pages/ActivityReport/Pages/index.js @@ -4,7 +4,6 @@ import topicsResources from './topicsResources'; import nextSteps from './nextSteps'; import goalsObjectives from './goalsObjectives'; import ReviewSubmit from './Review'; -import reviewItem from './Review/reviewItem'; /* Note these are not react nodes but objects used by the navigator to render out @@ -48,7 +47,11 @@ const reviewPage = { approvingManager={approvingManager} onResetToDraft={onResetToDraft} reviewItems={ - pages.map((p) => reviewItem(p.path, p.label, p.sections, formData)) + pages.map((p) => ({ + id: p.path, + title: p.label, + content: p.reviewSection(), + })) } formData={formData} pages={allPages} diff --git a/frontend/src/pages/ActivityReport/Pages/nextSteps.js b/frontend/src/pages/ActivityReport/Pages/nextSteps.js index 4f05a4d7a5..8b3a74aaf5 100644 --- a/frontend/src/pages/ActivityReport/Pages/nextSteps.js +++ b/frontend/src/pages/ActivityReport/Pages/nextSteps.js @@ -1,5 +1,6 @@ import React from 'react'; import { Helmet } from 'react-helmet'; +import ReviewPage from './Review/ReviewPage'; const NextSteps = () => ( <> @@ -16,12 +17,16 @@ NextSteps.propTypes = {}; const sections = []; +const ReviewSection = () => ( + +); + export default { position: 4, label: 'Next steps', path: 'next-steps', review: false, - sections, + reviewSection: () => , render: () => ( ), diff --git a/frontend/src/pages/ActivityReport/Pages/topicsResources.js b/frontend/src/pages/ActivityReport/Pages/topicsResources.js index 6f3ff4aa52..f49ceabb00 100644 --- a/frontend/src/pages/ActivityReport/Pages/topicsResources.js +++ b/frontend/src/pages/ActivityReport/Pages/topicsResources.js @@ -1,13 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; - import { Controller, useFormContext } from 'react-hook-form'; import { Helmet } from 'react-helmet'; - import { Fieldset, Label, TextInput, } from '@trussworks/react-uswds'; +import { isUndefined } from 'lodash'; +import Section from './Review/ReviewSection'; +import ReviewItem from './Review/ReviewItem'; +import FileReviewItem from './Review/FileReviewItem'; import MultiSelect from '../../../components/MultiSelect'; import FileUploader from '../../../components/FileUploader'; import FormItem from '../../../components/FormItem'; @@ -91,36 +93,77 @@ TopicsResources.propTypes = { reportId: PropTypes.node.isRequired, }; -const sections = [ - { - title: 'Topics covered', - anchor: 'topics-resources', - items: [ - { label: 'Topics', name: 'topics' }, - ], - }, - { - title: 'Resources', - anchor: 'resources', - items: [ - { label: 'Resources used', name: 'resourcesUsed' }, - { label: 'Other resources', name: 'otherResources', path: 'originalFileName' }, - ], - }, - { - title: 'Attachments', - anchor: 'attachments', - items: [ - { label: 'Attachments', name: 'attachments', path: 'originalFileName' }, - ], - }, -]; +const ReviewSection = () => { + const { watch } = useFormContext(); + const { + otherResources, + resourcesUsed, + attachments, + topics: formTopics, + } = watch(); + + const hasOtherResources = otherResources && otherResources.length > 0; + const hasAttachments = attachments && attachments.length > 0; + + return ( + <> +
+ +
+
+ + {otherResources.map((file) => ( + + ))} +
+
+ {attachments.map((file) => ( + + ))} +
+ + ); +}; export default { position: 2, label: 'Topics and resources', path: 'topics-resources', - sections, + reviewSection: () => , review: false, render: (additionalData, formData, reportId) => ( { describe('File Upload Handlers happy path', () => { beforeEach(() => { uploadFile.mockReset(); + getPresignedURL.mockReset(); }); it('tests a file upload', async () => { ActivityReportPolicy.mockImplementation(() => ({ canUpdate: () => true, })); + uploadFile.mockResolvedValue({ key: 'key' }); await request(app) .post('/api/files') .field('reportId', report.dataValues.id) diff --git a/src/services/activityReports.test.js b/src/services/activityReports.test.js index edf8e47f8f..5f4dc31ccb 100644 --- a/src/services/activityReports.test.js +++ b/src/services/activityReports.test.js @@ -253,8 +253,8 @@ describe('Activity Reports DB service', () => { it('retrieves all recipients when not specifying region', async () => { const recipients = await possibleRecipients(); - // 11 From db being seeded + 1 that we create for this test suite = 12 - expect(recipients.grants.length).toBe(12); + const grantees = await Grantee.findAll(); + expect(recipients.grants.length).toBe(grantees.length); }); }); });