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

add filters and target populations table to grantee record page #622

Merged
merged 5 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ parameters:
type: string
dev_git_branch: # change to feature branch to test deployment
description: "Name of github branch that will deploy to dev"
default: "js-fix-plotly-import"
default: "TTAHUB276"
type: string
sandbox_git_branch: # change to feature branch to test deployment
default: "kw-delete-reports-script"
default: "TTAHUB276"
type: string
prod_new_relic_app_id:
default: "877570491"
Expand Down Expand Up @@ -333,6 +333,15 @@ jobs:
- run:
name: Build backend assets
command: yarn build
- when:
condition:
and:
- equal: [<< pipeline.project.git_url >>, << pipeline.parameters.prod_git_url >>]
- equal: [<< pipeline.git.branch >>, << pipeline.parameters.prod_git_branch >>]
steps:
- run:
name: Create production robot
command: ./bin/robot-factory
- run:
name: Build frontend assets
command: yarn --cwd frontend run build
Expand Down
9 changes: 9 additions & 0 deletions bin/robot-factory
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

echo 'Setting site to be indexable in robots.txt'

cat >frontend/public/robots.txt <<EOL
# Welcome Robots
User-agent: *
Disallow:
EOL
6 changes: 4 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.11",
"@hookform/error-message": "^0.0.5",
"@trussworks/react-uswds": "1.11.0",
"@trussworks/react-uswds": "2.4.1",
"@use-it/interval": "^1.0.0",
"draft-js": "^0.11.7",
"draftjs-to-html": "^0.9.1",
Expand Down Expand Up @@ -157,7 +157,9 @@
"<rootDir>/src/pages/NotFound/index.js",
"<rootDir>/src/polyfills.js",
"<rootDir>/src/pages/Widgets/index.js",
"<rootDir>/src/widgets/Example.js"
"<rootDir>/src/widgets/Example.js",
"<rootDir>/src/pages/RegionalDashboard/formatDateRange.js",
"<rootDir>/src/pages/RegionalDashboard/constants.js"
],
"coverageThreshold": {
"global": {
Expand Down
5 changes: 4 additions & 1 deletion frontend/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# we disallow everything by default
# in production, we allow indexing by removing the slash from the disallow

# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
Disallow: /
1 change: 1 addition & 0 deletions frontend/src/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,5 @@ export const ESCAPE_KEY_CODE = 27;
export const ESCAPE_KEY_CODES = ['Escape', 'Esc'];

export const DATE_FMT = 'YYYY/MM/DD';
export const DATE_DISPLAY_FORMAT = 'MM/DD/YYYY';
export const EARLIEST_INC_FILTER_DATE = moment('2020-08-31');
122 changes: 122 additions & 0 deletions frontend/src/components/Accordion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';

export const AccordionItem = ({
title,
id,
content,
expanded,
className,
handleToggle,
headingSize,
}) => {
const headingClasses = `usa-accordion__heading ${className}`;
const contentClasses = `usa-accordion__content usa-prose ${className}`;
const HeadingSizeTag = `h${headingSize}`;
return (
<>
<HeadingSizeTag className={headingClasses}>
<button
type="button"
className="usa-accordion__button"
aria-expanded={expanded}
aria-controls={id}
data-testid={`accordionButton_${id}`}
onClick={handleToggle}
>
{title}
</button>
</HeadingSizeTag>
<div
id={id}
data-testid={`accordionItem_${id}`}
className={contentClasses}
hidden={!expanded}
>
{content}
</div>
</>
);
};

AccordionItem.propTypes = {
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
expanded: PropTypes.bool.isRequired,
id: PropTypes.string.isRequired,
className: PropTypes.string,
handleToggle: PropTypes.func,
headingSize: PropTypes.number.isRequired,
};

AccordionItem.defaultProps = {
className: '',
handleToggle: () => { },
};

export const Accordion = ({
bordered,
items,
multiselectable,
headingSize,
}) => {
const [openItems, setOpenState] = useState(
items.filter((i) => !!i.expanded).map((i) => i.id),
);

const classes = bordered ? 'usa-accordion usa-accordion--bordered' : 'usa-accordion';

const toggleItem = (itemId) => {
const newOpenItems = [...openItems];
const itemIndex = openItems.indexOf(itemId);
const isMultiselectable = multiselectable;

if (itemIndex > -1) {
newOpenItems.splice(itemIndex, 1);
} else if (isMultiselectable) {
newOpenItems.push(itemId);
} else {
newOpenItems.splice(0, newOpenItems.length);
newOpenItems.push(itemId);
}
setOpenState(newOpenItems);
};

return (
<div
className={classes}
data-testid="accordion"
aria-multiselectable={multiselectable || undefined}
>
{items.map((item) => (
<AccordionItem
key={`accordionItem_${item.id}`}
title={item.title}
id={item.id}
content={item.content}
className={item.className}
expanded={openItems.indexOf(item.id) > -1}
handleToggle={() => {
toggleItem(item.id);
}}
headingSize={headingSize}
/>
))}
</div>
);
};

Accordion.propTypes = {
bordered: PropTypes.bool,
multiselectable: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.shape(AccordionItem)).isRequired,
headingSize: PropTypes.number,
};

Accordion.defaultProps = {
bordered: false,
multiselectable: false,
headingSize: 2,
};

export default Accordion;
35 changes: 20 additions & 15 deletions frontend/src/components/ActivityReportsTable/ReportRow.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Tag, Checkbox,
} from '@trussworks/react-uswds';
import { Checkbox } from '@trussworks/react-uswds';
import { Link, useHistory } from 'react-router-dom';

import moment from 'moment';
import ContextMenu from '../ContextMenu';
import { getReportsDownloadURL } from '../../fetchers/helpers';
import TooltipWithCollection from '../TooltipWithCollection';
import Tooltip from '../Tooltip';
import { DATE_DISPLAY_FORMAT } from '../../Constants';

function ReportRow({
report, openMenuUp, handleReportSelect, isChecked,
Expand All @@ -22,6 +22,8 @@ function ReportRow({
collaborators,
lastSaved,
calculatedStatus,
approvedAt,
createdAt,
legacyId,
} = report;

Expand Down Expand Up @@ -85,24 +87,21 @@ function ReportRow({
</td>
<td>{startDate}</td>
<td>
<span className="smart-hub--ellipsis" title={authorName}>
{authorName}
</span>
<Tooltip
displayText={authorName}
tooltipText={authorName}
buttonLabel="click to reveal author name"
/>
</td>
<td>{moment(createdAt).format(DATE_DISPLAY_FORMAT)}</td>
<td>
<TooltipWithCollection collection={topics} collectionTitle={`topics for ${displayId}`} />
</td>
<td>
<TooltipWithCollection collection={collaboratorNames} collectionTitle={`collaborators for ${displayId}`} />
</td>
<td>{lastSaved}</td>
<td>
<Tag
className={`smart-hub--table-tag-status smart-hub--status-${calculatedStatus}`}
>
{calculatedStatus === 'needs_action' ? 'Needs action' : calculatedStatus}
</Tag>
</td>
<td>{approvedAt && moment(approvedAt).format(DATE_DISPLAY_FORMAT)}</td>
<td>
<ContextMenu label={contextMenuLabel} menuItems={menuItems} up={openMenuUp} />
</td>
Expand All @@ -122,14 +121,20 @@ export const reportPropTypes = {
}),
}),
})).isRequired,
approvedAt: PropTypes.string,
createdAt: PropTypes.string,
startDate: PropTypes.string.isRequired,
author: PropTypes.shape({
fullName: PropTypes.string,
homeRegionId: PropTypes.number,
name: PropTypes.string,
}).isRequired,
topics: PropTypes.arrayOf(PropTypes.string).isRequired,
collaborators: PropTypes.arrayOf(PropTypes.string).isRequired,
collaborators: PropTypes.arrayOf(
PropTypes.shape({
fullName: PropTypes.string,
}),
).isRequired,
lastSaved: PropTypes.string.isRequired,
calculatedStatus: PropTypes.string.isRequired,
legacyId: PropTypes.string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import '@testing-library/jest-dom';
import React from 'react';
import {
render, screen, act,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ColumnHeader from '../ColumnHeader';

describe('ActivityReportsTable ColumnHeader', () => {
const renderColumnHeader = (onUpdateSort = jest.fn(), sortDirection = 'asc') => {
const name = 'fanciest shoes';
render(
<div>
<table>
<thead>
<tr>
<ColumnHeader
onUpdateSort={onUpdateSort}
displayName={name}
name="shoes"
sortBy="shoes"
sortDirection={sortDirection}
/>
</tr>
</thead>
</table>
</div>,
);
};

it('renders and calls on update sort', async () => {
const onUpdateSort = jest.fn();
renderColumnHeader(onUpdateSort);

const shoes = await screen.findByText('fanciest shoes');

await act(async () => userEvent.click(shoes));
expect(onUpdateSort).toHaveBeenCalledWith('shoes');
});

it('sorts on keypress', async () => {
const onUpdateSort = jest.fn();
renderColumnHeader(onUpdateSort, 'desc');

const shoes = await screen.findByText('fanciest shoes');

await act(async () => userEvent.type(shoes, '{enter}'));
expect(onUpdateSort).toHaveBeenCalledTimes(2);
});

it('displays an unsortable column', async () => {
const onUpdateSort = jest.fn();
renderColumnHeader(onUpdateSort, '');

const shoes = await screen.findByRole('columnheader', { name: /fanciest shoes/i });
expect(shoes).toHaveAttribute('aria-sort', 'none');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import '@testing-library/jest-dom';
import React from 'react';
import {
render, screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Router } from 'react-router';
import { createMemoryHistory } from 'history';
import ReportRow from '../ReportRow';
import { generateXFakeReports } from '../mocks';

const history = createMemoryHistory();

const [report] = generateXFakeReports(1);

describe('ReportRow', () => {
const renderReportRow = () => (
render(
<Router history={history}>
<ReportRow
report={report}
openMenuUp={false}
handleReportSelect={jest.fn()}
isChecked={false}
/>
</Router>,
)
);

beforeAll(async () => {
global.navigator.clipboard = jest.fn();
global.navigator.clipboard.writeText = jest.fn(() => Promise.resolve());
});

afterAll(() => {
delete global.navigator;
});

it('the view link works', async () => {
history.push = jest.fn();
renderReportRow();
userEvent.click(await screen.findByRole('button', { name: 'Actions for activity report R14-AR-1' }));
userEvent.click(await screen.findByRole('button', { name: /view/i }));

expect(history.push).toHaveBeenCalled();
});

it('you can copy', async () => {
renderReportRow();
userEvent.click(await screen.findByRole('button', { name: 'Actions for activity report R14-AR-1' }));
userEvent.click(await screen.findByRole('button', { name: /copy url/i }));

expect(navigator.clipboard.writeText).toHaveBeenCalled();
});
});
Loading