-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* GRB participation banner * GetSystemIntakesWithReviewRequested query * Intakes table * Add requesterComponent to query * Table formatting * Table pagination * Unit tests * Unit test typo * Hide component behind flag * Mock data for table sorting * Fix project name column sorting * GRB date default sorting * Unit test for sorting function * Fix mock data * Divider
- Loading branch information
1 parent
5eb6b74
commit e667d3b
Showing
9 changed files
with
560 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import React from 'react'; | ||
import { MemoryRouter } from 'react-router-dom'; | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { | ||
GetSystemIntakesWithReviewRequestedDocument, | ||
GetSystemIntakesWithReviewRequestedQuery, | ||
SystemIntakeWithReviewRequestedFragment | ||
} from 'gql/gen/graphql'; | ||
import { DateTime } from 'luxon'; | ||
|
||
import { systemIntakesWithReviewRequested } from 'data/mock/systemIntake'; | ||
import { MockedQuery } from 'types/util'; | ||
import { formatDateLocal } from 'utils/date'; | ||
import { getPersonNameAndComponentAcronym } from 'utils/getPersonNameAndComponent'; | ||
import VerboseMockedProvider from 'utils/testing/VerboseMockedProvider'; | ||
|
||
import GrbParticipationNeeded, { sortGrbDates } from '.'; | ||
|
||
const mockSystemIntakes = systemIntakesWithReviewRequested; | ||
|
||
const getSystemIntakesWithReviewRequestedQuery = ( | ||
systemIntakes: SystemIntakeWithReviewRequestedFragment[] | ||
): MockedQuery<GetSystemIntakesWithReviewRequestedQuery> => ({ | ||
request: { | ||
query: GetSystemIntakesWithReviewRequestedDocument, | ||
variables: {} | ||
}, | ||
result: { | ||
data: { | ||
__typename: 'Query', | ||
systemIntakesWithReviewRequested: systemIntakes | ||
} | ||
} | ||
}); | ||
|
||
describe('GRB participation needed', () => { | ||
it('renders component if user is GRB reviewer', async () => { | ||
render( | ||
<VerboseMockedProvider | ||
mocks={[getSystemIntakesWithReviewRequestedQuery(mockSystemIntakes)]} | ||
> | ||
<GrbParticipationNeeded /> | ||
</VerboseMockedProvider> | ||
); | ||
|
||
expect( | ||
await screen.findByRole('heading', { name: 'GRB participation needed' }) | ||
); | ||
}); | ||
|
||
it('does not render component if user is not a GRB reviewer', () => { | ||
render( | ||
<VerboseMockedProvider | ||
mocks={[getSystemIntakesWithReviewRequestedQuery([])]} | ||
> | ||
<GrbParticipationNeeded /> | ||
</VerboseMockedProvider> | ||
); | ||
|
||
expect( | ||
screen.queryByRole('heading', { name: 'GRB participation needed' }) | ||
).toBeNull(); | ||
}); | ||
|
||
it('formats system intake for table', async () => { | ||
render( | ||
<MemoryRouter> | ||
<VerboseMockedProvider | ||
mocks={[getSystemIntakesWithReviewRequestedQuery(mockSystemIntakes)]} | ||
> | ||
<GrbParticipationNeeded /> | ||
</VerboseMockedProvider> | ||
</MemoryRouter> | ||
); | ||
|
||
const testIntake = mockSystemIntakes[0]; | ||
|
||
userEvent.click( | ||
await screen.findByRole('button', { name: 'Show GRB reviews' }) | ||
); | ||
|
||
expect( | ||
screen.getByRole('link', { name: testIntake.requestName! }) | ||
).toHaveAttribute( | ||
'href', | ||
`/governance-review-board/${testIntake.id}/grb-review` | ||
); | ||
|
||
expect( | ||
screen.getByText( | ||
getPersonNameAndComponentAcronym( | ||
testIntake.requesterName!, | ||
testIntake.requesterComponent | ||
) | ||
) | ||
).toBeInTheDocument(); | ||
|
||
expect( | ||
screen.getByText(formatDateLocal(testIntake.grbDate!, 'MM/dd/yyyy')) | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
// Check that sorting function used in table correctly sorts GRB dates | ||
it('sorts GRB dates', () => { | ||
const currentYear = DateTime.local().year; | ||
|
||
const dates = mockSystemIntakes.map(value => value.grbDate) as Array< | ||
string | null | ||
>; | ||
|
||
// sorted dates using function, reversed to get desc order to match table | ||
const sortedDates = dates.sort(sortGrbDates).reverse(); | ||
|
||
const expectedSortOrder = [ | ||
null, | ||
null, | ||
`${currentYear + 1}-06-09T03:11:24.478056Z`, | ||
`${currentYear + 2}-10-02T03:11:24.478056Z`, | ||
'2024-03-29T03:11:24.478056Z', | ||
'2023-01-18T03:11:24.478056Z', | ||
'2020-10-08T03:11:24.478056Z' | ||
]; | ||
|
||
sortedDates.forEach((date, index) => date === expectedSortOrder[index]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
import React, { useMemo, useState } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Column, usePagination, useSortBy, useTable } from 'react-table'; | ||
import { | ||
Button, | ||
IconGroups, | ||
IconVisibilityOff, | ||
IconVisiblity, | ||
Table | ||
} from '@trussworks/react-uswds'; | ||
import { | ||
SystemIntakeWithReviewRequestedFragment, | ||
useGetSystemIntakesWithReviewRequestedQuery | ||
} from 'gql/gen/graphql'; | ||
|
||
import UswdsReactLink from 'components/LinkWrapper'; | ||
import Divider from 'components/shared/Divider'; | ||
import IconButton from 'components/shared/IconButton'; | ||
import TablePagination from 'components/TablePagination'; | ||
import { formatDateLocal, isDateInPast } from 'utils/date'; | ||
import { getPersonNameAndComponentAcronym } from 'utils/getPersonNameAndComponent'; | ||
import { | ||
currentTableSortDescription, | ||
getColumnSortStatus, | ||
getHeaderSortIcon | ||
} from 'utils/tableSort'; | ||
|
||
/** | ||
* Sort GRB dates | ||
* | ||
* No date set - order first, | ||
* Future dates - closest dates first, | ||
* Past dates - order last | ||
*/ | ||
export const sortGrbDates = ( | ||
grbDateA: string | null, | ||
grbDateB: string | null | ||
) => { | ||
// Sort null dates first | ||
if (grbDateA === null) return 1; | ||
if (grbDateB === null) return -1; | ||
|
||
// Sort past dates | ||
if (isDateInPast(grbDateA) || isDateInPast(grbDateB)) { | ||
return grbDateA < grbDateB ? -1 : 1; | ||
} | ||
|
||
// Sort future dates | ||
if (grbDateA === grbDateB) return 0; | ||
return grbDateA > grbDateB ? -1 : 1; | ||
}; | ||
|
||
/** | ||
* GRB Participation Needed alert box with table of system intakes | ||
* | ||
* Only shows if GRB review has been requested from user on at least one intake | ||
*/ | ||
const GrbParticipationNeeded = () => { | ||
const { t } = useTranslation('grbReview'); | ||
|
||
// Toggles GRB reviews table | ||
const [showGrbReviews, setShowGrbReviews] = useState<boolean>(false); | ||
|
||
const { data, loading } = useGetSystemIntakesWithReviewRequestedQuery(); | ||
|
||
const systemIntakes = data?.systemIntakesWithReviewRequested || []; | ||
|
||
const columns = useMemo< | ||
Column<SystemIntakeWithReviewRequestedFragment>[] | ||
>(() => { | ||
return [ | ||
{ | ||
Header: t<string>('intake:fields.projectName'), | ||
accessor: 'requestName', | ||
Cell: cell => { | ||
const { row, value } = cell; | ||
|
||
return ( | ||
<UswdsReactLink | ||
to={`/governance-review-board/${row.original.id}/grb-review`} | ||
> | ||
{value} | ||
</UswdsReactLink> | ||
); | ||
} | ||
}, | ||
{ | ||
Header: t<string>('intake:fields.requester'), | ||
accessor: ({ requesterName, requesterComponent }) => | ||
requesterName | ||
? getPersonNameAndComponentAcronym( | ||
requesterName, | ||
requesterComponent | ||
) | ||
: '' | ||
}, | ||
{ | ||
Header: t<string>('homepage.grbDate'), | ||
accessor: 'grbDate', | ||
Cell: ({ value }) => | ||
value | ||
? formatDateLocal(value, 'MM/dd/yyyy') | ||
: t<string>('homepage.noDateSet'), | ||
sortType: (rowA, rowB) => | ||
sortGrbDates(rowA.values.grbDate, rowB.values.grbDate) | ||
} | ||
]; | ||
}, [t]); | ||
|
||
const table = useTable( | ||
{ | ||
columns, | ||
data: systemIntakes, | ||
autoResetSortBy: false, | ||
autoResetPage: true, | ||
initialState: { | ||
sortBy: useMemo(() => [{ id: 'grbDate', desc: true }], []), | ||
pageIndex: 0, | ||
pageSize: 5 | ||
} | ||
}, | ||
useSortBy, | ||
usePagination | ||
); | ||
|
||
const { | ||
getTableBodyProps, | ||
getTableProps, | ||
headerGroups, | ||
prepareRow, | ||
page, | ||
rows | ||
} = table; | ||
|
||
// Only show if user has been requested as reviewer | ||
if (loading || systemIntakes.length === 0) return null; | ||
|
||
return ( | ||
<> | ||
<div className="bg-primary-lighter padding-4"> | ||
<div className="display-flex flex-align-start flex-justify"> | ||
<h2 className="margin-y-0 margin-right-2"> | ||
{t('homepage.participationNeeded')} | ||
</h2> | ||
<IconGroups size={4} className="text-primary" /> | ||
</div> | ||
|
||
<p className="line-height-body-5 margin-top-1 margin-bottom-3"> | ||
{t('homepage.participationNeededText')} | ||
</p> | ||
|
||
{/* Toggle GRB reviews button */} | ||
<IconButton | ||
onClick={() => setShowGrbReviews(!showGrbReviews)} | ||
icon={showGrbReviews ? <IconVisibilityOff /> : <IconVisiblity />} | ||
type="button" | ||
unstyled | ||
> | ||
{showGrbReviews | ||
? t('homepage.hideGrbReviews') | ||
: t('homepage.showGrbReviews')} | ||
</IconButton> | ||
|
||
{showGrbReviews && ( | ||
<div className="margin-top-4 margin-bottom-neg-2"> | ||
<Table bordered={false} fullWidth {...getTableProps()}> | ||
<thead> | ||
{headerGroups.map(headerGroup => ( | ||
<tr {...headerGroup.getHeaderGroupProps()}> | ||
{headerGroup.headers.map((column, index) => ( | ||
<th | ||
{...column.getHeaderProps( | ||
column.getSortByToggleProps() | ||
)} | ||
aria-sort={getColumnSortStatus(column)} | ||
scope="col" | ||
className="border-bottom-2px bg-primary-lighter" | ||
> | ||
<Button | ||
type="button" | ||
unstyled | ||
className="width-full display-flex" | ||
{...column.getSortByToggleProps()} | ||
> | ||
<div className="flex-fill text-no-wrap"> | ||
{column.render('Header')} | ||
</div> | ||
<div className="position-relative width-205 margin-left-05"> | ||
{getHeaderSortIcon(column)} | ||
</div> | ||
</Button> | ||
</th> | ||
))} | ||
</tr> | ||
))} | ||
</thead> | ||
<tbody {...getTableBodyProps()}> | ||
{page.map(row => { | ||
prepareRow(row); | ||
return ( | ||
<tr {...row.getRowProps()}> | ||
{row.cells.map((cell, index) => { | ||
return ( | ||
<td | ||
{...cell.getCellProps()} | ||
className="bg-primary-lighter" | ||
> | ||
{cell.render('Cell')} | ||
</td> | ||
); | ||
})} | ||
</tr> | ||
); | ||
})} | ||
</tbody> | ||
</Table> | ||
|
||
{rows.length > 0 && ( | ||
<> | ||
<TablePagination | ||
{...table} | ||
pageIndex={table.state.pageIndex} | ||
pageSize={table.state.pageSize} | ||
page={[]} | ||
className="desktop:grid-col-fill desktop:padding-bottom-0" | ||
/> | ||
|
||
<div | ||
className="usa-sr-only usa-table__announcement-region" | ||
aria-live="polite" | ||
> | ||
{currentTableSortDescription(headerGroups[0])} | ||
</div> | ||
</> | ||
)} | ||
</div> | ||
)} | ||
</div> | ||
|
||
<Divider className="margin-top-6" /> | ||
</> | ||
); | ||
}; | ||
|
||
export default GrbParticipationNeeded; |
Oops, something went wrong.