diff --git a/.circleci/config.yml b/.circleci/config.yml index b9adf5ba70..206e1f7ff0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -228,7 +228,7 @@ jobs: - run: name: Run cucumber command: | - sleep 5 + sleep 10 yarn cucumber:ci - store_artifacts: path: reports/ @@ -240,12 +240,12 @@ jobs: - run: name: Start up local server command: | # production style build (single BE server with static FE) - docker-compose -f docker-compose-test.yml run --rm server yarn install --production=false - docker-compose -f docker-compose-test.yml run --rm server yarn --cwd frontend install --production=false - docker-compose -f docker-compose-test.yml run --rm server yarn build - docker-compose -f docker-compose-test.yml run --rm server yarn --cwd frontend run build - docker-compose -f docker-compose-test.yml up -d - docker-compose -f docker-compose-test.yml exec server yarn db:migrate:ci + docker-compose -f docker-compose.dss.yml run --rm server yarn install --production=false + docker-compose -f docker-compose.dss.yml run --rm server yarn --cwd frontend install --production=false + docker-compose -f docker-compose.dss.yml run --rm server yarn build + docker-compose -f docker-compose.dss.yml run --rm server yarn --cwd frontend run build + docker-compose -f docker-compose.dss.yml up -d + docker-compose -f docker-compose.dss.yml exec server yarn db:migrate:ci - run: name: Pull OWASP ZAP docker image command: docker pull owasp/zap2docker-weekly diff --git a/bin/run-tests b/bin/run-tests new file mode 100755 index 0000000000..a7c694c4d6 --- /dev/null +++ b/bin/run-tests @@ -0,0 +1,77 @@ +#!/bin/bash + +declare -a options=(frontend backend) +declare lr=">>>>>>>>" +declare -i exit_code=0 + +log() { + echo "$lr $*" +} + +check_exit() { + if [[ "$1" -ne 0 ]]; then + echo "$lr last docker-compose command failed" + ((exit_code++)) + fi +} + +main() { + local opt="" + + for o in "${options[@]}"; do + if [[ "${1}" == "$o" ]]; then + opt="$o"; + fi + done + + log "Running tests in using test config 'docker-compose.test.yml'" + # Start containers + docker-compose -f 'docker-compose.test.yml' up -d + check_exit "$?" + + # Let postgres initialize + echo + log "Giving postgres a few seconds to start up..." + sleep 5 + + # Migrate and seed db + echo + log "Migrating & seeding db" + docker exec test-backend bash -c "yarn db:migrate" + check_exit "$?" + docker exec test-backend bash -c "yarn db:seed;" + check_exit "$?" + + if [[ "$opt" == "backend" || -z "$opt" ]]; then + # Test backend + echo + log "Running backend tests" + docker exec test-backend bash -c "yarn test:ci" + check_exit "$?" + fi + + if [[ "$opt" == "frontend" || -z "$opt" ]]; then + # Test frontend + echo + log "Running frontend tests" + docker exec test-frontend bash -c "yarn --cwd frontend run test:ci" + check_exit "$?" + fi + + # Cleanup + echo + log "Cleaning up test containers" + docker-compose \ + -f 'docker-compose.test.yml' \ + down --volumes + check_exit "$?" + + if [[ $exit_code -ne 0 ]]; then + echo + log "Errors occurred during script execution" + fi + + exit "$exit_code" +} + +main "$@" diff --git a/docker-compose-test.yml b/docker-compose.dss.yml similarity index 100% rename from docker-compose-test.yml rename to docker-compose.dss.yml diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000000..46d93e3edc --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,39 @@ +version: "3.5" +services: + test-backend: + build: + context: . + container_name: test-backend + command: yarn server + user: ${CURRENT_USER:-root} + depends_on: + - test-db + environment: + - POSTGRES_HOST=test-db + volumes: + - ".:/app:rw" + networks: + - ttadp-test + test-frontend: + build: + context: . + container_name: test-frontend + command: yarn start + user: ${CURRENT_USER:-root} + stdin_open: true + volumes: + - "./frontend:/app:rw" + - "./scripts:/app/scripts" + environment: + - BACKEND_PROXY=http://test-backend:8080 + networks: + - ttadp-test + test-db: + image: postgres:12.4 + container_name: test-db + env_file: .env + networks: + - ttadp-test +# Use non-default network so we don't conflict with the developer environment +networks: + ttadp-test: diff --git a/docs/logical_data_model.md b/docs/logical_data_model.md index 8ba0a9b8f5..3f8880c41a 100644 --- a/docs/logical_data_model.md +++ b/docs/logical_data_model.md @@ -1,7 +1,7 @@ Logical Data Model ================== - + UML Source ---------- @@ -150,6 +150,7 @@ class ActivityReport { ttaType : array context : string pageState : json + managerNotes : string * userId : integer(32) REFERENCES public.Users.id * lastUpdatedById : integer(32) REFERENCES public.Users.id * regionId : integer(32) REFERENCES public.Region.id @@ -208,7 +209,7 @@ NonGrantee ||-{ ActivityParticipant Instructions ------------ -1. [Edit this diagram with plantuml.com](http://www.plantuml.com/plantuml/uml/nLVRRkCs47tNL_2jRO0OHOkYm29Ox6xgBV9GDqYo7p2M9Z9RYiCEHzaQSVwzffWAeYnd6QkNbmGwSpyyrCZll01BsvZ2bs1GVRluyOUY-4h10-bAEQQrmSQhpres2cnji58bqsUlxX6byBzhwu2XKKPRbAr3HImRAehIWFVgaqTMZuLIPqfNwetILh3UGnSNDLfa4ApUljsRDnvzyBqAJbvOstgFMcXm-EmsP77LDcla8Noci05jXf1eCVX6eMsRa9qQlC5QaxqQDF7rzzzzeswM7vStozNbybQvTcrqkOYEVg6hBdN96BfyB_j0qGrwWSPrbjPL4vQdlqUFUNA6FniDsgUuBSdXAHcKgj2Nh9reiZqwQvnhv4PxdprIwv4ps0w7tdFD9vwH3pnbX7Ly_XCd-qUBNfRCn3w73NBXGPCTdb3BUlZOe6meKJ_7CX-esgQmTsGz7iClgS8UdpaecIdvlIJPEtAwp8_foyIuB6KncPp33bdxivQG_ghwKapNnNo1_4nWyZXGoWj8wuT8I7ZF8pVjFp8rjmsm-0sYt0bS6mY2MXCP13klusxmbzgkzxOCqaEA_ujq1YkOgTVvbOxM6Q-dazzgTHmeX_Ne6GCF1TLj-9z0S3GeskeGdKrCa436HJvbm9IQtBWOTPA6Pt2n8XkxFcM1polzlXJze6LxWuvOSiepUcgvxBx1aXq4LQN3uWAp8i54LL7elzvV0uikjGjC9tUXGgCVaBU_76osgktNOOMbRi0aQWp39RBj8UWdsnVKQELkwt1YPuw_3m_lX6Eg6PeG7t1WXkr5j0P21BX6kIRNcgxJAHz6y6Gd32vbP6IEhyL9Wq2Svy1IaXNyKv81eCPRYUpyvicU_D5bmCkdAAWFst_vY-qvRBwq1wBBkrwGt8cCXjDB51j_rMidXjZ-DP0Hj-gTPnELjdz9ni2Q68Jul-BhjO8C8TFjvw-KkMsVi2cUsmuJgDtkd1vLb7f4dkZyVFSu-F5KnDzqXtYCcZAGwVsxJwMY-wkUTXqKUYYYQdgtVWyNuSy16bCk5Yhy7wISXcBHFji8YON3v7s9vr5IBEKv93ckq4ZToovVofVqUymKAHoPaxCScTB7n2Tv9qLUWdtQt5zqFExp3ctLDkOl) +1. [Edit this diagram with plantuml.com](http://www.plantuml.com/plantuml/png/nLVRRkCs47tNL_2jRO0OHOkYm29Ox6xgBV9GDqYo7p2M9Z9RYiCEHzaQSVwzefaAeYnd6QkNbmGwSpyylBXuvmLOsiOKlWI3wjl5ZpyKnLSA7aXNoZ6j3JPUVTQoKM1hWvKgcJvxTOyeXVzTMmSCZJBQecmTAcBPK56K1hnNdpooUYoKEbDTjZTAMy6wWoukQhJ885YzVTkPDnvzyBqAJbvOsxf3BJGu_7ORihXgcpNo43vJs80sGqYq67oZqBPDo4uDtc2joLuDclZw----KJVBZykRvUfoUQjSkpQwN4H7lz3LFdN96BfyBrsWw0QzGEFsbjPL4vQdlqUFUNA6FniDsgUuBSdXAHcKgj2Nh9reiZqwQvnhv4PxdprIwv4ps0wBtdFD9wwH3pnbXEtv_2TMze-smvRCn3w73NBXGvCThb3BUlZPe6meKJ_7CX-esgQmTsGz7iClgS8UdpaecIdvlIJPEtAwp8_foyIuB6KncPp53bdxjPQG_ghwKio-Y_a2-Pd0v76Wb1UGrW-HaF2UHsxQVsHgRXjWyHj4kHEuDX04jIOo27PMnt7PNcgxxcmPf8SK_nVf35OmKw_pAnsjCxwUJZxLwZXG3klHCmQE11KHbfz0S3GeskeGdKrCa43EHZvXm9IQtBWOTPA6Pt2n8XkxFkK2drVwVIdwGSls1Xsnv9HdzDHokN-39NSGL9KMJHRCYWGJLKMX_tf_3Iowr2umdUo52er-GDx-Sd2pLMr_7LPOwWPCeiOmN2BR7e9-igwWHYjtMuSJT-RuVNYu9nnJpT24-803CsmleZKG8i0roZMvrkmxdV9X14zsmk0IHiRcU2nE6WJYF0UMagpWdv8C03NUIcJdDq-sWngm7SZfIfty63FWvLCKseVjl_oIUmwRB_c16FEs7IHx9QFXT7F5nbzr7CKGsnyF6N6htdasKMdxbsGCh8b1YFyhlbuho10otNx-LJEtxHDkaEUsmmHgjpkdHnMb7f4dEZ_VFGv-L1NnjzuX7gEc36Hw__qJgeXlwAVT7Wez554rlLj_FnVXpm4QKouMAlmVf9o6Oj4-smY9XSFaVOddKL8ivJaaEQxGIDsBBb_Cb_Hxp1Gf79cJjHoPqi_49pcJ8Y_1FcqUF1gE-hn3MxMD-Gi0) 2. Copy and paste the final UML into the UML Source section 3. Update the img src and edit link target to the current values diff --git a/docs/openapi/paths/activity-reports/review.yaml b/docs/openapi/paths/activity-reports/review.yaml new file mode 100644 index 0000000000..0156f6d371 --- /dev/null +++ b/docs/openapi/paths/activity-reports/review.yaml @@ -0,0 +1,46 @@ +put: + tags: + - activity-reports + summary: Review an activity report + description: > + An approving manager reviews an activity report to determine if it requires + any additional updates. If the report needs updates the manager sets the status to + 'Needs Action', otherwise to 'Approved' + requestBody: + description: The status and any manager notes + required: true + content: + application/json: + schema: + type: object + properties: + status: + type: string + description: The status of the report after review + enum: + - Approved + - Needs Action + managerNotes: + type: string + description: Any notes the manager needs to relay to the author/collaborators of the report + parameters: + - in: path + name: activityReportId + required: true + schema: + type: number + responses: + 200: + description: The new status of the activity report + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - Approved + - Needs Action + managerNotes: + type: string diff --git a/docs/openapi/paths/collaborators.yaml b/docs/openapi/paths/collaborators.yaml index 8c0bd058a1..547f39767d 100644 --- a/docs/openapi/paths/collaborators.yaml +++ b/docs/openapi/paths/collaborators.yaml @@ -20,4 +20,4 @@ get: schema: type: array items: - $ref: '../../index.yaml#/components/schemas/selectableUser' + $ref: '../index.yaml#/components/schemas/selectableUser' diff --git a/docs/openapi/paths/index.yaml b/docs/openapi/paths/index.yaml index d3b0a8f797..af6eaf2bce 100644 --- a/docs/openapi/paths/index.yaml +++ b/docs/openapi/paths/index.yaml @@ -22,5 +22,7 @@ $ref: './activity-reports/activity-reports-id.yaml' '/activity-reports/{activityReportId}/submit': $ref: './activity-reports/submit.yaml' +'/activity-reports/{activityReportId}/review': + $ref: './activity-reports/review.yaml' '/files': $ref: './files.yaml' diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000000..5268d7fe57 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,48 @@ +# Testing + +## Tips and Caveats when writing tests + + +### Handling async/promises + +When writing tests that rely on asynchronous operations, such as writing to the database, take care to make sure that those operations are resolved before any tests that rely on them run. If you need to create database records in a setup function such as `beforeAll`, you will want to make sure all async/promise operations resolve before subsequent tests run. You can make sure multiple await (promise) operations resolve by using [`Promise.all()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) (which takes an iterable of promises). + +Here's how that might look: + +``` +let a = someAsyncFun(); +let b = anotherAsyncFun(); + +return Promise.all([a, b]); +``` + +### Creating/deleting database records + +Some tests will require interactions with the database, but at present all tests run using the same instance of the database. That means that any records you create or delete can potentially affect tests elsewhere. On top of that, tests run in parallel, so database operations may run in an unexpected order. That can mean tests may pass various times only to fail due to missing or unexpected records in the database when your tests run. + +To mitigate issues with missing or unexpected records causing failing tests, you can try a few approaches. One approach is to avoid using the database if your test doesn't actually require it. You may be able to use mock models or responses rather than interact with the database. If your test does require the database, you should create the records you need before your tests run and delete the records you created (and no others) when your tests finish. If you `Model.create`, make sure you `Model.destroy()` the records you created. + +When writing tests that create database records, it might also help to use a `try...catch` to catch errors in database transactions and log meaningful output. Sequelize error messages can be vague, and it might help others to see more informative messages. + +## Testing in Docker + +### Using `./bin/run-tests` + +To simplify running tests in Docker, there is a bash script, `./bin/run-tests` that will run the appropriate commands to start `test-` variations of the services used in tests. You should be able to run tests using that command while your development Docker environment is running. The script uses a separate `docker-compose.test.yml` which does not create a user-accessible network and cleans up after itself once tests have run. + +This script is written such that it will log errors, but won't exit if a docker command fails. It will count the number of errors and the number of errors will be the exit code (`$?`) for the script. So if three docker commands fail, the exit code would be 3. + +By default, `./bin/run-tests` will run both backend and frontend tests. If you want to run only one set of tests, supply 'frontend' or 'backend' as a parameter. So to run only the backend tests, you'd run `./bin/run-tests backend`. + +### Running tests in your development Docker environment + +When running tests in Docker, be aware that there are tests that will modify/delete database records. For tests to run, the 'db' service needs to exist and `db:migrate` and `db:seed` need to have been run (to create the tables and populate certain records). + +In the `docker-compose.yml` configuration, the database is set up to persist to a volume, "dbdata", so database records will persist between runs of the 'db' service, unless you remove that volume explicitly (e.g. `docker volume rm` or `docker-compose down --volumes`). + + +### Notes on docker-compose and multiple configurations + +`docker-compose` has a feature for providing multiple `docker-compose.*.yml` files where subsequent files can override settings in previous files, which sounds like it would suit the use case of running docker for local development and for testing. However, the ability to [override configurations](https://docs.docker.com/compose/extends/#adding-and-overriding-configuration) is limited. While experimenting with overrides, it became clear that doing so would require a minimum of three docker-compose.yml files: one "base", one for local development, one for running tests. Trying to compose docker-compose.yml files would be complicated. + +In addition, while experimenting with multiple configuration files, it became clear that docker was unable to differentiate between different versions of the same service. Trying to override the 'db' service for testing would not work as expected: if the local/dev 'db' service had already been created, that one would be used when tests were run. diff --git a/frontend/src/App.js b/frontend/src/App.js index c992efaef2..f218439bcf 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -75,8 +75,8 @@ function App() { /> ( - + render={({ match, location }) => ( + )} /> {admin diff --git a/frontend/src/Constants.js b/frontend/src/Constants.js index 9bc3f54ac6..df2b978d90 100644 --- a/frontend/src/Constants.js +++ b/frontend/src/Constants.js @@ -64,3 +64,5 @@ export const REGIONS = [ 11, 12, ]; + +export const DECIMAL_BASE = 10; diff --git a/frontend/src/components/Navigator/__tests__/index.js b/frontend/src/components/Navigator/__tests__/index.js index fde326316e..a11f34e5f7 100644 --- a/frontend/src/components/Navigator/__tests__/index.js +++ b/frontend/src/components/Navigator/__tests__/index.js @@ -42,7 +42,7 @@ const pages = [ label: 'review page', path: 'review', review: true, - render: (hookForm, allComplete, formData, submitted, onSubmit) => ( + render: (hookForm, allComplete, formData, onSubmit) => (
@@ -59,6 +59,9 @@ describe('Navigator', () => { {}} + approvingManager={false} defaultValues={{ first: '', second: '' }} pages={pages} currentPage={currentPage} diff --git a/frontend/src/components/Navigator/components/SideNav.css b/frontend/src/components/Navigator/components/SideNav.css index 685fc85f03..d917223677 100644 --- a/frontend/src/components/Navigator/components/SideNav.css +++ b/frontend/src/components/Navigator/components/SideNav.css @@ -49,8 +49,13 @@ color: #f8f8f8; } +.smart-hub--tag-needs-action { + background-color: #f9e0e4; + color: #d42240; +} + .smart-hub--tag { - width: 84px; + width: 94px; text-align: center; font-weight: normal; display: inline-block; diff --git a/frontend/src/components/Navigator/components/SideNav.js b/frontend/src/components/Navigator/components/SideNav.js index f1e172ff23..7de4990ba3 100644 --- a/frontend/src/components/Navigator/components/SideNav.js +++ b/frontend/src/components/Navigator/components/SideNav.js @@ -13,7 +13,7 @@ import moment from 'moment'; import Container from '../../Container'; import './SideNav.css'; import { - NOT_STARTED, IN_PROGRESS, COMPLETE, SUBMITTED, + NOT_STARTED, IN_PROGRESS, COMPLETE, SUBMITTED, APPROVED, NEEDS_ACTION, } from '../constants'; const tagClass = (state) => { @@ -26,6 +26,10 @@ const tagClass = (state) => { return 'smart-hub--tag-complete'; case SUBMITTED: return 'smart-hub--tag-submitted'; + case APPROVED: + return 'smart-hub--tag-submitted'; + case NEEDS_ACTION: + return 'smart-hub--tag-needs-action'; default: return ''; } @@ -46,7 +50,7 @@ function SideNav({ > {page.label} - {page.state + {page.state !== 'draft' && ( {page.state} diff --git a/frontend/src/components/Navigator/constants.js b/frontend/src/components/Navigator/constants.js index 35cbee5d85..b644ef86cb 100644 --- a/frontend/src/components/Navigator/constants.js +++ b/frontend/src/components/Navigator/constants.js @@ -2,3 +2,5 @@ export const NOT_STARTED = 'Not started'; export const IN_PROGRESS = 'In progress'; export const COMPLETE = 'Complete'; export const SUBMITTED = 'Submitted'; +export const APPROVED = 'Approved'; +export const NEEDS_ACTION = 'Needs Action'; diff --git a/frontend/src/components/Navigator/index.js b/frontend/src/components/Navigator/index.js index a0cf8a44b0..a2ce7c76bb 100644 --- a/frontend/src/components/Navigator/index.js +++ b/frontend/src/components/Navigator/index.js @@ -15,29 +15,31 @@ import moment from 'moment'; import Container from '../Container'; import { - IN_PROGRESS, COMPLETE, SUBMITTED, + IN_PROGRESS, COMPLETE, } from './constants'; import SideNav from './components/SideNav'; import NavigatorHeader from './components/NavigatorHeader'; function Navigator({ initialData, + initialLastUpdated, pages, onFormSubmit, - submitted, + onReview, currentPage, additionalData, onSave, autoSaveInterval, + approvingManager, + status, reportId, }) { const [formData, updateFormData] = useState(initialData); const [errorMessage, updateErrorMessage] = useState(); - const [lastSaveTime, updateLastSaveTime] = useState(); + const [lastSaveTime, updateLastSaveTime] = useState(initialLastUpdated); const { pageState } = formData; const page = pages.find((p) => p.path === currentPage); - const submittedNavState = submitted ? SUBMITTED : null; const allComplete = _.every(pageState, (state) => state === COMPLETE); const hookForm = useForm({ @@ -76,8 +78,8 @@ function Navigator({ const result = await onSave(data, newIndex); if (result) { updateLastSaveTime(moment()); + updateErrorMessage(); } - updateErrorMessage(); } catch (error) { // eslint-disable-next-line no-console console.log(error); @@ -102,7 +104,7 @@ function Navigator({ const navigatorPages = pages.map((p) => { const current = p.position === page.position; const stateOfPage = current ? IN_PROGRESS : pageState[p.position]; - const state = p.review ? submittedNavState : stateOfPage; + const state = p.review ? status : stateOfPage; return { label: p.label, onNavigation: () => onSaveForm(false, p.position), @@ -129,9 +131,10 @@ function Navigator({ hookForm, allComplete, formData, - submitted, onFormSubmit, additionalData, + onReview, + approvingManager, reportId, )} {!page.review @@ -157,9 +160,12 @@ function Navigator({ Navigator.propTypes = { initialData: PropTypes.shape({}), + initialLastUpdated: PropTypes.instanceOf(moment), onFormSubmit: PropTypes.func.isRequired, - submitted: PropTypes.bool.isRequired, onSave: PropTypes.func.isRequired, + status: PropTypes.string.isRequired, + onReview: PropTypes.func.isRequired, + approvingManager: PropTypes.bool.isRequired, pages: PropTypes.arrayOf( PropTypes.shape({ review: PropTypes.bool.isRequired, @@ -179,6 +185,7 @@ Navigator.defaultProps = { initialData: {}, additionalData: {}, autoSaveInterval: 1000 * 60 * 2, + initialLastUpdated: null, }; export default Navigator; diff --git a/frontend/src/fetchers/Admin.js b/frontend/src/fetchers/Admin.js index 2ea28e313d..7897c8792e 100644 --- a/frontend/src/fetchers/Admin.js +++ b/frontend/src/fetchers/Admin.js @@ -1,5 +1,6 @@ import join from 'url-join'; import { get, put } from './index'; +import { DECIMAL_BASE } from '../Constants'; export const getUsers = async () => { const users = await get((join('/', 'api', 'admin', 'users'))); @@ -7,6 +8,6 @@ export const getUsers = async () => { }; export const updateUser = async (userId, data) => { - const user = await put((join('/', 'api', 'admin', 'users', userId.toString(10))), data); + const user = await put((join('/', 'api', 'admin', 'users', userId.toString(DECIMAL_BASE))), data); return user.json(); }; diff --git a/frontend/src/fetchers/activityReports.js b/frontend/src/fetchers/activityReports.js index 5e4e92f4b3..ace889f1c5 100644 --- a/frontend/src/fetchers/activityReports.js +++ b/frontend/src/fetchers/activityReports.js @@ -1,5 +1,6 @@ import join from 'url-join'; import { get, put, post } from './index'; +import { DECIMAL_BASE } from '../Constants'; const activityReportUrl = join('/', 'api', 'activity-reports'); @@ -9,13 +10,13 @@ export const getApprovers = async (region) => { }; export const submitReport = async (reportId, data) => { - const url = join(activityReportUrl, reportId.toString(10), 'submit'); + const url = join(activityReportUrl, reportId.toString(DECIMAL_BASE), 'submit'); const report = await post(url, data); return report.json(); }; export const saveReport = async (reportId, data) => { - const report = await put(join(activityReportUrl, reportId.toString(10)), data); + const report = await put(join(activityReportUrl, reportId.toString(DECIMAL_BASE)), data); return report.json(); }; @@ -25,7 +26,7 @@ export const createReport = async (data) => { }; export const getReport = async (reportId) => { - const report = await get(join(activityReportUrl, reportId.toString(10))); + const report = await get(join(activityReportUrl, reportId.toString(DECIMAL_BASE))); return report.json(); }; @@ -39,3 +40,9 @@ export const getCollaborators = async (region) => { const collaborators = await get(url); return collaborators.json(); }; + +export const reviewReport = async (reportId, data) => { + const url = join(activityReportUrl, reportId.toString(DECIMAL_BASE), 'review'); + const report = await put(url, data); + return report.json(); +}; diff --git a/frontend/src/pages/ActivityReport/Pages/ApproverReviewPage.js b/frontend/src/pages/ActivityReport/Pages/ApproverReviewPage.js new file mode 100644 index 0000000000..96856aa8ec --- /dev/null +++ b/frontend/src/pages/ActivityReport/Pages/ApproverReviewPage.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Dropdown, Form, Label, Fieldset, Textarea, Alert, Button, +} from '@trussworks/react-uswds'; + +const possibleStatus = [ + 'Approved', + 'Needs Action', +]; + +const ApproverReviewPage = ({ + reviewed, + additionalNotes, + register, + valid, + handleSubmit, + onFormReview, +}) => ( + <> + {reviewed + && ( + + Success +
+ Your review of this report was successfully submitted +
+ )} +

Review and approve report

+
+

+ Creator notes +
+
+ { additionalNotes || 'No creator notes' } +

+
+
+
+ +