Skip to content

Commit

Permalink
Merge pull request #185 from adhocteam/js-247-topic-resource-review-p…
Browse files Browse the repository at this point in the history
…age-download

Refactor activity report review pages
  • Loading branch information
jasalisbury authored Mar 4, 2021
2 parents dc13faa + ac1a896 commit 92a929b
Show file tree
Hide file tree
Showing 24 changed files with 461 additions and 200 deletions.
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ body {
}

@media print {
.smart-hub-header {
position: relative;
}

* {
color: #000 !important;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/FileUploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -34,6 +34,7 @@ describe('SideNav', () => {
pages={pages}
skipTo="skip"
skipToMessage="message"
errorMessage={errorMessage}
/>,
);
};
Expand All @@ -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');
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Navigator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function Navigator({
const hookForm = useForm({
mode: 'onChange',
defaultValues: formData,
shouldUnregister: false,
});

const {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/__tests__/FileUploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/fetchers/__tests__/activityReports.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
32 changes: 32 additions & 0 deletions frontend/src/pages/ActivityReport/Pages/Review/FileReviewItem.js
Original file line number Diff line number Diff line change
@@ -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 (
<Grid row className="margin-top-1 ">
<Grid col={6}>
{filename}
</Grid>
<Grid col={6}>
<div className="flex-align-end display-flex flex-column no-print">
{approved
&& <a href={url}>Download</a>}
{!approved
&& <div>Not Approved</div>}
</div>
</Grid>
</Grid>
);
};

FileReviewItem.propTypes = {
filename: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
};

export default FileReviewItem;
48 changes: 48 additions & 0 deletions frontend/src/pages/ActivityReport/Pages/Review/ReviewItem.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`grid-row ${classes} margin-bottom-3 desktop:margin-bottom-0`}>
<div className="grid-col-12 desktop:grid-col-6 font-sans-2xs desktop:font-sans-sm text-bold desktop:text-normal">
{label}
</div>
<div className="grid-col-12 desktop:grid-col-6">
{values.map((v, index) => (
<div aria-label={`${label} ${index + 1}`} key={`${label}${v}`} col={12} className="desktop:flex-align-end display-flex flex-column flex-justify-center">
{Number.isNaN(v) ? '' : v}
</div>
))}
</div>
</div>
);
};

ReviewItem.propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
path: PropTypes.string,
};

ReviewItem.defaultProps = {
path: '',
};

export default ReviewItem;
53 changes: 53 additions & 0 deletions frontend/src/pages/ActivityReport/Pages/Review/ReviewPage.js
Original file line number Diff line number Diff line change
@@ -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
hidePrint={isEmpty}
key={section.title}
basePath={path}
anchor={section.anchor}
title={section.title}
>
{section.items.map((item) => (
<ReviewItem
key={item.label}
label={item.label}
path={item.path}
name={item.name}
/>
))}
</Section>
);
})}
</>
);
};

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;
44 changes: 44 additions & 0 deletions frontend/src/pages/ActivityReport/Pages/Review/ReviewSection.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classes}>
<div className="grid-row border-bottom padding-bottom-1 margin-bottom-105">
<div className="grid-col-12 desktop:grid-col-6">
<b className="margin-y-1">{title}</b>
</div>
<div className="grid-col-12 desktop:grid-col-6 display-flex flex-align-end flex-column flex-justify-center">
<HashLink
aria-label={`Edit form section "${title}"`}
to={`${basePath}#${anchor}`}
className="smart-hub-edit-link pull-right no-print"
>
Edit
</HashLink>
</div>
</div>
{children}
</div>
);
};

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;
Original file line number Diff line number Diff line change
@@ -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' }) => (
<FileReviewItem
filename="filename"
url="http://localhost:3000"
status={status}
/>
);

describe('ReviewPage', () => {
it('displays the filename', async () => {
render(<RenderFileReviewItem />);
const item = await screen.findByText('filename');
expect(item).toBeVisible();
});

it('displays the download link', async () => {
render(<RenderFileReviewItem />);
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(<RenderFileReviewItem status="" />);
const status = await screen.findByText('Not Approved');
expect(status).toBeVisible();
});
});
Loading

0 comments on commit 92a929b

Please sign in to comment.