Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TTTA-HUB 22: Add User Permission to Unlock Approved Reports #478

Merged
merged 10 commits into from
Oct 28, 2021
22 changes: 22 additions & 0 deletions docs/openapi/paths/activity-reports/unlock.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
put:
tags:
- activity-reports
summary: Unlocks approved activity reports
description: >
When a user has the UNLOCK_REPORT global permission they are able to unlock an approved activity report.
Once the report is unlocked, the report and approving manager statuses will be set to needs_action.
parameters:
- in: path
name: activityReportId
required: true
schema:
type: number
responses:
204:
description: The report has been successfully unlocked
content:
application/json:
schema:
$ref: '../../index.yaml#/components/schemas/activityReport'
403:
description: User doesn't have permission to unlock the activity report
2 changes: 2 additions & 0 deletions docs/openapi/paths/index.yaml
Original file line number Diff line number Diff line change
@@ -38,6 +38,8 @@
$ref: './activity-reports/activity-reports-id.yaml'
'/activity-reports/{activityReportId}/submit':
$ref: './activity-reports/submit.yaml'
'/activity-reports/{activityReportId}/unlock':
$ref: './activity-reports/unlock.yaml'
'/activity-reports/{activityReportId}/review':
$ref: './activity-reports/review.yaml'
'/activity-reports/{activityReportId}/reset':
5 changes: 5 additions & 0 deletions frontend/src/Constants.js
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ export const SCOPE_IDS = {
READ_WRITE_ACTIVITY_REPORTS: 3,
READ_ACTIVITY_REPORTS: 4,
APPROVE_ACTIVITY_REPORTS: 5,
UNLOCK_APPROVED_REPORTS: 6,
};

export const REGIONAL_SCOPES = {
@@ -32,6 +33,10 @@ export const GLOBAL_SCOPES = {
name: 'ADMIN',
description: 'User can view the admin panel and change user permissions (including their own)',
},
[SCOPE_IDS.UNLOCK_APPROVED_REPORTS]: {
name: 'UNLOCK_APPROVED_REPORTS',
description: 'User can unlock approved reports.',
},
};

export const ROLES = [
4 changes: 4 additions & 0 deletions frontend/src/__tests__/permissions.js
Original file line number Diff line number Diff line change
@@ -34,6 +34,10 @@ describe('permissions', () => {
scopeId: SCOPE_IDS.SITE_ACCESS,
regionId: 14,
},
{
scopeId: SCOPE_IDS.UNLOCK_APPROVED_REPORTS,
regionId: 14,
},
{
scopeId: SCOPE_IDS.SITE_ACCESS,
regionId: 1,
26 changes: 26 additions & 0 deletions frontend/src/components/Modal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#popup-modal div {
border-style: none;
padding: 3px 0px 17px 15px;
margin: 7px 0px 0px 3px;
text-align: left;
line-height: 1.2;
font-size: 14px;
}

#popup-modal h2 {
font-size: 22px;
margin-top: 20px;
margin-bottom: -10px;
}

#popup-modal button {
margin-left: 0px;
margin-right: 14px;
padding: 9px 38px 9px 38px;
font-size: medium;
font-weight: 600;
}

#popup-modal .usa-button--secondary {
background-color: #D42240;
}
68 changes: 68 additions & 0 deletions frontend/src/components/Modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { Button, Modal as TrussWorksModal } from '@trussworks/react-uswds';
import { ESCAPE_KEY_CODES } from '../Constants';
import './Modal.css';

const Modal = ({
onOk, onClose, closeModal, title, okButtonText, okButtonAriaLabel, children,
}) => {
const modalRef = useRef(null);

const onEscape = useCallback((event) => {
if (ESCAPE_KEY_CODES.includes(event.key)) {
closeModal();
}
}, [closeModal]);

useEffect(() => {
document.addEventListener('keydown', onEscape, false);
return () => {
document.removeEventListener('keydown', onEscape, false);
};
}, [onEscape]);

useEffect(() => {
const button = modalRef.current.querySelector('button');
if (button) {
button.focus();
}
});

return (
<div className="popup-modal" ref={modalRef} aria-modal="true" role="dialog" id="popup-modal">
<TrussWorksModal
title={<h2>{title}</h2>}
actions={(
<>
<Button type="button" onClick={onClose}>
Cancel
</Button>

<Button type="button" aria-label={okButtonAriaLabel} secondary onClick={onOk}>
{okButtonText}
</Button>
</>
)}
>
{children}
</TrussWorksModal>
</div>
);
};

Modal.propTypes = {
onOk: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
okButtonText: PropTypes.string.isRequired,
okButtonAriaLabel: PropTypes.string,
children: PropTypes.node.isRequired,
};

Modal.defaultProps = {
okButtonAriaLabel: null,
};

export default Modal;
74 changes: 74 additions & 0 deletions frontend/src/components/__tests__/Modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import '@testing-library/jest-dom';
import React from 'react';
import {
render, screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useModal, connectModal, Button } from '@trussworks/react-uswds';
import Modal from '../Modal';

const SomeComponent = () => {
const { isOpen, openModal, closeModal } = useModal();
const ConnectModal = connectModal(Modal);

return (
<div>
<ConnectModal
onUnlock={() => {}}
onClose={() => {}}
closeModal={closeModal}
isOpen={isOpen}
/>
<Button onClick={openModal}>Open</Button>
</div>
);
};

describe('Modal', () => {
it('shows two buttons', async () => {
// Given a page with a modal
render(<Modal
onUnlock={() => {}}
onClose={() => {}}
closeModal={() => {}}
isOpen
/>);
// When the modal is triggered
const buttons = await screen.findAllByRole('button');

// Then we see our options
expect(buttons.length).toBe(2);
});

it('exits when escape key is pressed', async () => {
// Given a page with a modal
render(<SomeComponent />);

// When the modal is triggered
const button = await screen.findByText('Open');
userEvent.click(button);

const modal = await screen.findByTestId('modal');
expect(modal).toBeVisible();

// And the modal can closeclose the modal via the escape key
userEvent.type(modal, '{esc}', { skipClick: true });
expect(screen.queryByTestId('modal')).not.toBeTruthy();
});

it('does not escape when any other key is pressed', async () => {
// Given a page with a modal
render(<SomeComponent />);

// When the modal is triggered
const button = await screen.findByText('Open');
userEvent.click(button);

const modal = await screen.findByTestId('modal');
expect(modal).toBeVisible();

// And the modal can close the modal via the escape key
userEvent.type(modal, '{enter}', { skipClick: true });
expect(screen.queryByTestId('modal')).toBeTruthy();
});
});
6 changes: 6 additions & 0 deletions frontend/src/fetchers/activityReports.js
Original file line number Diff line number Diff line change
@@ -32,6 +32,12 @@ export const deleteReport = async (reportId) => {
await destroy(join(activityReportUrl, reportId.toString(DECIMAL_BASE)));
};

export const unlockReport = async (reportId) => {
const url = join(activityReportUrl, reportId.toString(DECIMAL_BASE), 'unlock');
const response = await put(url);
return response.status;
};

export const createReport = async (data) => {
const report = await post(activityReportUrl, data);
return report.json();
7 changes: 7 additions & 0 deletions frontend/src/pages/Admin/__tests__/PermissionHelpers.js
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ const {
ADMIN,
READ_ACTIVITY_REPORTS,
READ_WRITE_ACTIVITY_REPORTS,
UNLOCK_APPROVED_REPORTS,
} = SCOPE_IDS;

describe('PermissionHelpers', () => {
@@ -73,10 +74,15 @@ describe('PermissionHelpers', () => {
scopeId: ADMIN,
regionId: 14,
},
{
scopeId: UNLOCK_APPROVED_REPORTS,
regionId: 14,
},
],
};
const globalPermissions = userGlobalPermissions(user);
expect(globalPermissions['2']).toBeTruthy();
expect(globalPermissions['6']).toBeTruthy();
});

it('flags global permissions the user does not have as false', () => {
@@ -85,6 +91,7 @@ describe('PermissionHelpers', () => {
};
const globalPermissions = userGlobalPermissions(user);
expect(globalPermissions['2']).toBeFalsy();
expect(globalPermissions['6']).toBeFalsy();
});
});
});
21 changes: 15 additions & 6 deletions frontend/src/pages/Admin/__tests__/UserPermissions.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import { SCOPE_IDS, DECIMAL_BASE } from '../../../Constants';
const {
READ_ACTIVITY_REPORTS,
ADMIN,
UNLOCK_APPROVED_REPORTS,
} = SCOPE_IDS;

describe('UserPermissions', () => {
@@ -18,8 +19,8 @@ describe('UserPermissions', () => {
render(<UserPermissions
regionalPermissions={{}}
globalPermissions={{}}
onRegionalPermissionChange={() => {}}
onGlobalPermissionChange={() => {}}
onRegionalPermissionChange={() => { }}
onGlobalPermissionChange={() => { }}
/>);
});

@@ -38,15 +39,23 @@ describe('UserPermissions', () => {
}}
globalPermissions={{
[ADMIN]: true,
[UNLOCK_APPROVED_REPORTS]: true,
}}
onRegionalPermissionChange={() => {}}
onGlobalPermissionChange={() => {}}
onRegionalPermissionChange={() => { }}
onGlobalPermissionChange={() => { }}
/>);
});

it('has correct global permissions checked', () => {
const checkbox = screen.getByRole('checkbox', { checked: true });
expect(checkbox.name).toBe(ADMIN.toString(DECIMAL_BASE));
const adminCheckbox = screen.getByRole('checkbox', {
name: /admin : user can view the admin panel and change user permissions \(including their own\)/i, checked: true,
});
expect(adminCheckbox.name).toBe(ADMIN.toString(DECIMAL_BASE));

const unlockCheckbox = screen.getByRole('checkbox', {
name: /unlock_approved_reports : user can unlock approved reports\./i, checked: true,
});
expect(unlockCheckbox.name).toBe(UNLOCK_APPROVED_REPORTS.toString(DECIMAL_BASE));
});

it('displays the current regional permissions', () => {
18 changes: 14 additions & 4 deletions frontend/src/pages/Admin/__tests__/UserSection.js
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import { SCOPE_IDS } from '../../../Constants';
const {
ADMIN,
READ_ACTIVITY_REPORTS,
UNLOCK_APPROVED_REPORTS,
} = SCOPE_IDS;

describe('UserSection', () => {
@@ -28,6 +29,10 @@ describe('UserSection', () => {
regionId: 14,
scopeId: ADMIN,
},
{
regionId: 14,
scopeId: UNLOCK_APPROVED_REPORTS,
},
{
regionId: 1,
scopeId: READ_ACTIVITY_REPORTS,
@@ -47,10 +52,15 @@ describe('UserSection', () => {
});

it('properly controls global permissions', () => {
const checkbox = screen.getByRole('checkbox', { name: /admin : user can view the admin panel and change user permissions \(including their own\)/i });
expect(checkbox).toBeChecked();
userEvent.click(checkbox);
expect(checkbox).not.toBeChecked();
const adminCheckbox = screen.getByRole('checkbox', { name: /admin : user can view the admin panel and change user permissions \(including their own\)/i });
expect(adminCheckbox).toBeChecked();
userEvent.click(adminCheckbox);
expect(adminCheckbox).not.toBeChecked();

const unlockCheckbox = screen.getByRole('checkbox', { name: /unlock_approved_reports : user can unlock approved reports\./i });
expect(unlockCheckbox).toBeChecked();
userEvent.click(unlockCheckbox);
expect(unlockCheckbox).not.toBeChecked();
});

it('properly controls regional permissions', () => {
Loading