From eb9c86c479151be8f809e3aa5afd98d042bb6529 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 12 Dec 2024 13:03:54 +0000 Subject: [PATCH] [ASAP-565] - add compliance dashboard (#4458) * add compliance dashboard * add tests * add ComplianceTable test * minor fix * update test * update compliance tab condition * Update apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx Co-authored-by: Gabriela Ueno --------- Co-authored-by: Gabriela Ueno --- .../src/network/ProfileSwitch.tsx | 16 +- .../src/network/teams/Compliance.tsx | 56 ++++ .../src/network/teams/TeamProfile.tsx | 24 +- .../teams/__tests__/TeamProfile.test.tsx | 65 +++++ apps/crn-frontend/src/network/teams/state.ts | 8 + packages/model/src/analytics.ts | 4 +- packages/model/src/manuscript.ts | 49 ++++ packages/react-components/src/index.ts | 1 + .../src/organisms/ComplianceTable.tsx | 275 ++++++++++++++++++ .../__tests__/ComplianceTable.test.tsx | 87 ++++++ .../react-components/src/organisms/index.ts | 1 + .../src/templates/ComplianceDashboard.tsx | 103 +++++++ .../src/templates/TeamProfileHeader.tsx | 11 +- .../__tests__/ComplianceDashboard.test.tsx | 45 +++ .../__tests__/TeamProfileHeader.test.tsx | 23 ++ .../react-components/src/templates/index.ts | 1 + packages/routing/src/network.ts | 4 + 17 files changed, 767 insertions(+), 6 deletions(-) create mode 100644 apps/crn-frontend/src/network/teams/Compliance.tsx create mode 100644 packages/react-components/src/organisms/ComplianceTable.tsx create mode 100644 packages/react-components/src/organisms/__tests__/ComplianceTable.test.tsx create mode 100644 packages/react-components/src/templates/ComplianceDashboard.tsx create mode 100644 packages/react-components/src/templates/__tests__/ComplianceDashboard.test.tsx diff --git a/apps/crn-frontend/src/network/ProfileSwitch.tsx b/apps/crn-frontend/src/network/ProfileSwitch.tsx index 812b58c238..7ed69f5518 100644 --- a/apps/crn-frontend/src/network/ProfileSwitch.tsx +++ b/apps/crn-frontend/src/network/ProfileSwitch.tsx @@ -13,7 +13,12 @@ const loadEventsList = () => const EventsList = lazy(loadEventsList); type RequiredPaths = 'about' | 'upcoming' | 'past'; -type OptionalPaths = 'calendar' | 'outputs' | 'workspace' | 'draftOutputs'; +type OptionalPaths = + | 'calendar' + | 'outputs' + | 'workspace' + | 'draftOutputs' + | 'compliance'; type ProfileSwitchProps = { About: FC; @@ -27,6 +32,7 @@ type ProfileSwitchProps = { paths: Record & Partial>; type: ComponentProps['type']; Workspace?: FC; + Compliance?: FC; }; const ProfileSwitch: FC = ({ @@ -41,6 +47,7 @@ const ProfileSwitch: FC = ({ type, Workspace, DraftOutputs, + Compliance, }) => ( @@ -71,6 +78,13 @@ const ProfileSwitch: FC = ({ )} + {Compliance && ( + + + + + + )} {isActive && ( diff --git a/apps/crn-frontend/src/network/teams/Compliance.tsx b/apps/crn-frontend/src/network/teams/Compliance.tsx new file mode 100644 index 0000000000..3bd39b7c86 --- /dev/null +++ b/apps/crn-frontend/src/network/teams/Compliance.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import { SearchField, ComplianceDashboard } from '@asap-hub/react-components'; +import { + complianceInitialSortingDirection, + ComplianceSortingDirection, + SortCompliance, +} from '@asap-hub/model'; + +import { SearchFrame } from '@asap-hub/frontend-utils'; +import { useManuscripts } from './state'; + +import { usePagination, usePaginationParams, useSearch } from '../../hooks'; + +const Compliance: React.FC = () => { + const { currentPage, pageSize } = usePaginationParams(); + const { filters, searchQuery, setSearchQuery } = useSearch(); + const result = useManuscripts({ + searchQuery, + currentPage, + pageSize, + filters, + }); + const { numberOfPages, renderPageHref } = usePagination( + result.total, + pageSize, + ); + + const [sort, setSort] = useState('team_asc'); + + const [sortingDirection, setSortingDirection] = + useState(complianceInitialSortingDirection); + + return ( +
+ + + + +
+ ); +}; + +export default Compliance; diff --git a/apps/crn-frontend/src/network/teams/TeamProfile.tsx b/apps/crn-frontend/src/network/teams/TeamProfile.tsx index c52a5c61b9..be16733ee6 100644 --- a/apps/crn-frontend/src/network/teams/TeamProfile.tsx +++ b/apps/crn-frontend/src/network/teams/TeamProfile.tsx @@ -22,7 +22,7 @@ import { useUpcomingAndPastEvents } from '../events'; import ProfileSwitch from '../ProfileSwitch'; import { ManuscriptToastProvider } from './ManuscriptToastProvider'; -import { useIsComplianceReviewer, useTeamById } from './state'; +import { useIsComplianceReviewer, useManuscripts, useTeamById } from './state'; import TeamManuscript from './TeamManuscript'; import { EligibilityReasonProvider } from './EligibilityReasonProvider'; import TeamComplianceReport from './TeamComplianceReport'; @@ -33,6 +33,8 @@ const loadOutputs = () => import(/* webpackChunkName: "network-team-outputs" */ './Outputs'); const loadWorkspace = () => import(/* webpackChunkName: "network-team-workspace" */ './Workspace'); +const loadCompliance = () => + import(/* webpackChunkName: "network-team-compliance" */ './Compliance'); const loadTeamOutput = () => import(/* webpackChunkName: "network-team-team-output" */ './TeamOutput'); const loadEventsList = () => @@ -41,6 +43,7 @@ const loadEventsList = () => const About = lazy(loadAbout); const Outputs = lazy(loadOutputs); const Workspace = lazy(loadWorkspace); +const Compliance = lazy(loadCompliance); const TeamOutput = lazy(loadTeamOutput); type TeamProfileProps = { @@ -76,15 +79,18 @@ const TeamProfile: FC = ({ currentTime }) => { const team = useTeamById(teamId); const user = useCurrentUserCRN(); const isStaff = user?.role === 'Staff'; + const isAsapTeam = team?.displayName === 'ASAP'; + const canDisplayCompliancePage = isStaff && isAsapTeam; useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises loadAbout() .then(team?.tools || isStaff ? loadWorkspace : undefined) + .then(canDisplayCompliancePage ? loadCompliance : undefined) .then(loadOutputs) .then(loadTeamOutput) .then(loadEventsList); - }, [team, isStaff]); + }, [team, isStaff, canDisplayCompliancePage]); const canShareResearchOutput = useCanShareResearchOutput( 'teams', @@ -118,9 +124,17 @@ const TeamProfile: FC = ({ currentTime }) => { teamId, }); + const manuscriptCount = useManuscripts({ + searchQuery: '', + currentPage: 0, + pageSize, + filters: new Set(), + }); + if (team) { const { about, + compliance, createOutput, duplicateOutput, outputs, @@ -133,6 +147,7 @@ const TeamProfile: FC = ({ currentTime }) => { }); const paths = { about: path + about.template, + compliance: path + compliance.template, outputs: path + outputs.template, past: path + past.template, upcoming: path + upcoming.template, @@ -199,6 +214,7 @@ const TeamProfile: FC = ({ currentTime }) => { = ({ currentTime }) => { teamDraftOutputsCount={ canShareResearchOutput ? outputDraftResults.total : undefined } + manuscriptsCount={manuscriptCount.total || 0} > ( @@ -233,6 +250,9 @@ const TeamProfile: FC = ({ currentTime }) => { Workspace={() => ( )} + {...(canDisplayCompliancePage + ? { Compliance: () => } + : {})} />
diff --git a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx index 2ede2975f1..717447d12a 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx @@ -815,3 +815,68 @@ describe('The draft output tab', () => { expect(screen.getByText('Draft Outputs (0)')).toBeVisible(); }); }); + +describe('The compliance tab', () => { + it('does not show compliance tab if not on Team ASAP', async () => { + enable('DISPLAY_MANUSCRIPTS'); + await renderPage({ + ...createTeamResponse(), + displayName: 'Test', + }); + + expect( + screen.queryByText(/Compliance/i, { selector: 'nav *' }), + ).not.toBeInTheDocument(); + }); + + it('does not show compliance tab if on Team ASAP but not Staff', async () => { + enable('DISPLAY_MANUSCRIPTS'); + await renderPage( + { + ...createTeamResponse(), + displayName: 'ASAP', + }, + {}, + { role: 'Grantee' }, + ); + + expect( + screen.queryByText(/Compliance/i, { selector: 'nav *' }), + ).not.toBeInTheDocument(); + }); + + it('shows compliance tab on Team ASAP page if user is Staff', async () => { + enable('DISPLAY_MANUSCRIPTS'); + await renderPage( + { + ...createTeamResponse(), + displayName: 'ASAP', + }, + {}, + { + role: 'Staff', + }, + ); + + expect( + screen.getByText(/Compliance/i, { selector: 'nav *' }), + ).toBeVisible(); + }); + + it('renders compliance dashboard on Team ASAP page', async () => { + enable('DISPLAY_MANUSCRIPTS'); + await renderPage( + { + ...createTeamResponse(), + displayName: 'ASAP', + }, + {}, + { + role: 'Staff', + }, + ); + + userEvent.click(screen.getByText(/Compliance/i, { selector: 'nav *' })); + expect(await screen.findByText(/No manuscripts available/i)).toBeVisible(); + }); +}); diff --git a/apps/crn-frontend/src/network/teams/state.ts b/apps/crn-frontend/src/network/teams/state.ts index 6cdb872eab..0d0bb4a6c7 100644 --- a/apps/crn-frontend/src/network/teams/state.ts +++ b/apps/crn-frontend/src/network/teams/state.ts @@ -11,6 +11,7 @@ import { ManuscriptPutRequest, DiscussionPatchRequest, DiscussionResponse, + ListPartialManuscriptResponse, } from '@asap-hub/model'; import { useCurrentUserCRN } from '@asap-hub/react-context'; import { useCallback } from 'react'; @@ -310,3 +311,10 @@ export const useReplyToDiscussion = () => { setDiscussion(discussion); }; }; + +export const useManuscripts = ( + options: GetListOptions, +): ListPartialManuscriptResponse => ({ + total: 0, + items: [], +}); diff --git a/packages/model/src/analytics.ts b/packages/model/src/analytics.ts index e5fb9f883b..04e70a0893 100644 --- a/packages/model/src/analytics.ts +++ b/packages/model/src/analytics.ts @@ -72,8 +72,8 @@ export type LeadershipAndMembershipFields = export type SortingDirection = 'asc' | 'desc'; -const ascending: SortingDirection = 'asc'; -const descending: SortingDirection = 'desc'; +export const ascending: SortingDirection = 'asc'; +export const descending: SortingDirection = 'desc'; export const initialSortingDirection = { team: ascending, diff --git a/packages/model/src/manuscript.ts b/packages/model/src/manuscript.ts index 51224ef84f..816f0904a2 100644 --- a/packages/model/src/manuscript.ts +++ b/packages/model/src/manuscript.ts @@ -1,5 +1,7 @@ import { JSONSchemaType } from 'ajv'; +import { ascending, descending, SortingDirection } from './analytics'; import { AuthorAlgoliaResponse, AuthorResponse } from './authors'; +import { ListResponse } from './common'; import { ComplianceReportDataObject } from './compliance-report'; import { DiscussionDataObject } from './discussion'; import { UserResponse } from './user'; @@ -783,3 +785,50 @@ export const asapFundingReasons = [ 'The manuscript is a thought leadership piece (review, communication, letter) pertaining to knowledge gaps in the field that the ASAP-funded proposal was addressing.', }, ]; + +export type SortComplianceFields = + | 'team' + | 'id' + | 'lastUpdated' + | 'status' + | 'apcCoverage'; + +export type ComplianceSortingDirection = { + [key in SortComplianceFields]: SortingDirection; +}; + +export type SortCompliance = + | 'team_asc' + | 'team_desc' + | 'id_asc' + | 'id_desc' + | 'last_updated_asc' + | 'last_updated_desc' + | 'status_asc' + | 'status_desc' + | 'apc_coverage_asc' + | 'apc_coverage_desc'; + +export const complianceInitialSortingDirection = { + team: ascending, + id: descending, + lastUpdated: descending, + status: ascending, + apcCoverage: ascending, +}; + +export type PartialManuscriptResponse = Pick< + ManuscriptVersion, + 'id' | 'requestingApcCoverage' +> & + Pick & { + lastUpdated: string; + team: { id: string; displayName: string }; + assignedUsers: Pick< + UserResponse, + 'id' | 'firstName' | 'lastName' | 'avatarUrl' + >[]; + }; + +export type ListPartialManuscriptResponse = + ListResponse; diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index d783385ea1..ef2dd3fe77 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -225,6 +225,7 @@ export { AnalyticsPageHeader, BasicLayout, BiographyModal, + ComplianceDashboard, ComplianceReportForm, ContactInfoModal, ContentPage, diff --git a/packages/react-components/src/organisms/ComplianceTable.tsx b/packages/react-components/src/organisms/ComplianceTable.tsx new file mode 100644 index 0000000000..905a0dff74 --- /dev/null +++ b/packages/react-components/src/organisms/ComplianceTable.tsx @@ -0,0 +1,275 @@ +import { + ComplianceSortingDirection, + complianceInitialSortingDirection, + SortCompliance, + PartialManuscriptResponse, +} from '@asap-hub/model'; +import { network } from '@asap-hub/routing'; +import { css } from '@emotion/react'; +import { Avatar, Card, Link } from '../atoms'; +import { borderRadius } from '../card'; +import { charcoal, neutral200, steel } from '../colors'; +import { AlphabeticalSortingIcon, NumericalSortingIcon } from '../icons'; +import { rem, tabletScreen } from '../pixels'; + +const container = css({ + display: 'grid', + paddingTop: rem(32), +}); + +const gridTitleStyles = css({ + display: 'none', + [`@media (min-width: ${tabletScreen.min}px)`]: { + display: 'inherit', + paddingBottom: rem(16), + }, +}); + +const rowTitleStyles = css({ + paddingTop: rem(32), + paddingBottom: rem(16), + ':first-of-type': { paddingTop: 0 }, + [`@media (min-width: ${tabletScreen.min}px)`]: { display: 'none' }, +}); + +const rowStyles = css({ + display: 'grid', + padding: `${rem(20)} ${rem(24)} 0`, + borderBottom: `1px solid ${steel.rgb}`, + ':first-of-type': { + borderBottom: 'none', + }, + ':nth-of-type(2n+3)': { + background: neutral200.rgb, + }, + ':last-child': { + borderBottom: 'none', + marginBottom: 0, + paddingBottom: rem(15), + borderRadius: rem(borderRadius), + }, + [`@media (min-width: ${tabletScreen.min}px)`]: { + gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr 1fr', + columnGap: rem(15), + paddingTop: 0, + paddingBottom: 0, + borderBottom: `1px solid ${steel.rgb}`, + }, +}); + +const titleStyles = css({ + display: 'flex', + alignItems: 'center', + fontWeight: 'bold', + color: charcoal.rgb, + gap: rem(8), +}); + +const teamNameStyles = css({ + display: 'flex', + gap: rem(3), +}); + +const buttonStyles = css({ + width: rem(24), + margin: 0, + padding: 0, + border: 'none', + backgroundColor: 'unset', + cursor: 'pointer', + alignSelf: 'center', +}); + +type ComplianceTableProps = { + data: PartialManuscriptResponse[]; + sort: SortCompliance; + setSort: React.Dispatch>; + sortingDirection: ComplianceSortingDirection; + setSortingDirection: React.Dispatch< + React.SetStateAction + >; +}; + +const ComplianceTable: React.FC = ({ + data, + sort, + sortingDirection, + setSort, + setSortingDirection, +}) => { + const isTeamSortActive = sort.includes('team'); + const isIdSortActive = sort.includes('id'); + const isLastUpdatedSortActive = sort.includes('last_updated'); + const isStatusSortActive = sort.includes('status'); + const isApcCoverageSortActive = sort.includes('apc_coverage'); + + return ( + +
+
+ + Team + + + + + ID + + + + Last Updated + + + + + Status + + + + APC Coverage + + + Assigned Users +
+ {data.map((row) => ( +
+ Team +

+ + {row.team.displayName} + +

+ ID +

{row.id}

+ Last Updated +

{row.lastUpdated}

+ Status +

{row.status}

+ APC Coverage +

{row.requestingApcCoverage}

+ Assigned Users +
+ {row.assignedUsers.map((user) => ( + + ))} +
+
+ ))} +
+
+ ); +}; + +export default ComplianceTable; diff --git a/packages/react-components/src/organisms/__tests__/ComplianceTable.test.tsx b/packages/react-components/src/organisms/__tests__/ComplianceTable.test.tsx new file mode 100644 index 0000000000..f80ef03b0d --- /dev/null +++ b/packages/react-components/src/organisms/__tests__/ComplianceTable.test.tsx @@ -0,0 +1,87 @@ +import { + complianceInitialSortingDirection, + PartialManuscriptResponse, +} from '@asap-hub/model'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ComponentProps } from 'react'; +import ComplianceTable from '../ComplianceTable'; + +describe('ComplianceTable', () => { + const pageControlsProps = { + numberOfPages: 1, + currentPageIndex: 0, + renderPageHref: () => '', + }; + + const complianceData: PartialManuscriptResponse = { + id: '1', + lastUpdated: '', + status: 'Addendum Required', + team: { id: 'team-id', displayName: 'Test Team' }, + requestingApcCoverage: 'Yes', + assignedUsers: [ + { + id: 'user-id', + firstName: 'Test', + lastName: 'User', + avatarUrl: 'https://example.com', + }, + ], + }; + + const defaultProps: ComponentProps = { + ...pageControlsProps, + data: [complianceData], + sort: 'team_asc', + setSort: jest.fn(), + sortingDirection: complianceInitialSortingDirection, + setSortingDirection: jest.fn(), + }; + + it('renders data', () => { + const { getByText } = render(); + expect(getByText('Test Team')).toBeInTheDocument(); + }); + + it.each` + sort | sortingDirection | iconTitle | newSort | newSortingDirection + ${'team_asc'} | ${{ ...complianceInitialSortingDirection, team: 'asc' }} | ${'Team Active Alphabetical Ascending Sort Icon'} | ${'team_desc'} | ${{ ...complianceInitialSortingDirection, team: 'desc' }} + ${'team_desc'} | ${{ ...complianceInitialSortingDirection, team: 'desc' }} | ${'Team Active Alphabetical Descending Sort Icon'} | ${'team_asc'} | ${{ ...complianceInitialSortingDirection, team: 'asc' }} + ${'id_asc'} | ${{ ...complianceInitialSortingDirection, id: 'asc' }} | ${'Team Inactive Alphabetical Ascending Sort Icon'} | ${'team_asc'} | ${{ ...complianceInitialSortingDirection, team: 'asc' }} + ${'team_desc'} | ${{ ...complianceInitialSortingDirection, id: 'desc' }} | ${'ID Inactive Numerical Descending Sort Icon'} | ${'id_desc'} | ${{ ...complianceInitialSortingDirection, id: 'desc' }} + ${'id_asc'} | ${{ ...complianceInitialSortingDirection, id: 'asc' }} | ${'ID Active Numerical Ascending Sort Icon'} | ${'id_desc'} | ${{ ...complianceInitialSortingDirection, id: 'desc' }} + ${'id_desc'} | ${{ ...complianceInitialSortingDirection, id: 'desc' }} | ${'ID Active Numerical Descending Sort Icon'} | ${'id_asc'} | ${{ ...complianceInitialSortingDirection, id: 'asc' }} + ${'team_desc'} | ${{ ...complianceInitialSortingDirection, lastUpdated: 'desc' }} | ${'Last Updated Inactive Numerical Descending Sort Icon'} | ${'last_updated_desc'} | ${{ ...complianceInitialSortingDirection, lastUpdated: 'desc' }} + ${'last_updated_asc'} | ${{ ...complianceInitialSortingDirection, lastUpdated: 'asc' }} | ${'Last Updated Active Numerical Ascending Sort Icon'} | ${'last_updated_desc'} | ${{ ...complianceInitialSortingDirection, lastUpdated: 'desc' }} + ${'last_updated_desc'} | ${{ ...complianceInitialSortingDirection, lastUpdated: 'desc' }} | ${'Last Updated Active Numerical Descending Sort Icon'} | ${'last_updated_asc'} | ${{ ...complianceInitialSortingDirection, lastUpdated: 'asc' }} + ${'team_desc'} | ${{ ...complianceInitialSortingDirection, status: 'asc' }} | ${'Status Inactive Alphabetical Ascending Sort Icon'} | ${'status_asc'} | ${{ ...complianceInitialSortingDirection, status: 'asc' }} + ${'status'} | ${{ ...complianceInitialSortingDirection, status: 'asc' }} | ${'Status Active Alphabetical Ascending Sort Icon'} | ${'status_desc'} | ${{ ...complianceInitialSortingDirection, status: 'desc' }} + ${'status'} | ${{ ...complianceInitialSortingDirection, status: 'desc' }} | ${'Status Active Alphabetical Descending Sort Icon'} | ${'status_asc'} | ${{ ...complianceInitialSortingDirection, status: 'asc' }} + ${'team_desc'} | ${{ ...complianceInitialSortingDirection, apcCoverage: 'asc' }} | ${'APC Coverage Inactive Alphabetical Ascending Sort Icon'} | ${'apc_coverage_asc'} | ${{ ...complianceInitialSortingDirection, apcCoverage: 'asc' }} + ${'apc_coverage_asc'} | ${{ ...complianceInitialSortingDirection, apcCoverage: 'asc' }} | ${'APC Coverage Active Alphabetical Ascending Sort Icon'} | ${'apc_coverage_desc'} | ${{ ...complianceInitialSortingDirection, apcCoverage: 'desc' }} + ${'apc_coverage_desc'} | ${{ ...complianceInitialSortingDirection, apcCoverage: 'desc' }} | ${'APC Coverage Active Alphabetical Descending Sort Icon'} | ${'apc_coverage_asc'} | ${{ ...complianceInitialSortingDirection, apcCoverage: 'asc' }} + `( + 'when sort is $sort and user clicks on $iconTitle, the new sort becomes $newSort and the sorting direction $newSortingDirection', + ({ sort, sortingDirection, iconTitle, newSort, newSortingDirection }) => { + const setSort = jest.fn(); + const setSortingDirection = jest.fn(); + const { getByTitle } = render( + , + ); + + const sortIcon = getByTitle(iconTitle); + expect(sortIcon).toBeInTheDocument(); + + userEvent.click(sortIcon); + expect(setSort).toHaveBeenCalledWith(newSort); + expect(setSortingDirection).toHaveBeenCalledWith(newSortingDirection); + }, + ); +}); diff --git a/packages/react-components/src/organisms/index.ts b/packages/react-components/src/organisms/index.ts index 49f1a0c7a5..094250c561 100644 --- a/packages/react-components/src/organisms/index.ts +++ b/packages/react-components/src/organisms/index.ts @@ -6,6 +6,7 @@ export { default as CaptionCard } from './CaptionCard'; export { default as CheckboxGroup } from './CheckboxGroup'; export { default as ComingSoon } from './ComingSoon'; export { default as ComplianceReportHeader } from './ComplianceReportHeader'; +export { default as ComplianceTable } from './ComplianceTable'; export { default as ConfirmModal } from './ConfirmModal'; export { default as CookiesModal } from './CookiesModal'; export { default as DashboardRecommendedUsers } from './DashboardRecommendedUsers'; diff --git a/packages/react-components/src/templates/ComplianceDashboard.tsx b/packages/react-components/src/templates/ComplianceDashboard.tsx new file mode 100644 index 0000000000..ae5075e5f6 --- /dev/null +++ b/packages/react-components/src/templates/ComplianceDashboard.tsx @@ -0,0 +1,103 @@ +import { + ComplianceSortingDirection, + manuscriptStatus, + PartialManuscriptResponse, + SortCompliance, +} from '@asap-hub/model'; +import { css } from '@emotion/react'; +import { ComponentProps } from 'react'; + +import { article, PageControls } from '..'; +import { Card, Headline3, Paragraph, Tag } from '../atoms'; +import { ComplianceTable } from '../organisms'; +import { rem } from '../pixels'; + +const pageControlsStyles = css({ + justifySelf: 'center', + paddingTop: rem(36), + paddingBottom: rem(36), +}); + +const iconStyles = css({ + display: 'inline-flex', + svg: { + width: rem(48), + height: rem(48), + path: { + fill: '#00202C', + }, + }, +}); + +const cardStyles = css({ + marginTop: rem(32), +}); + +const statusDescriptionStyles = css({ + fontWeight: 'bold', + marginBottom: rem(16), +}); + +const manuscriptStatusContainerStyles = css({ + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: '16px', + justifyItems: 'start', +}); + +type ComplianceDashboardProps = ComponentProps & { + data: PartialManuscriptResponse[]; + sort: SortCompliance; + setSort: React.Dispatch>; + sortingDirection: ComplianceSortingDirection; + setSortingDirection: React.Dispatch< + React.SetStateAction + >; +}; + +const ComplianceDashboard: React.FC = ({ + data, + sort, + sortingDirection, + setSort, + setSortingDirection, + ...pageControlsProps +}) => ( +
+ +
+ Manuscripts by status: +
+
+ {manuscriptStatus.map((status, index) => ( + {status} + ))} +
+
+ {data.length > 0 ? ( +
+ +
+ +
+
+ ) : ( +
+ {article} + No manuscripts available. + + When a team shares a manuscript for a compliance review, it will be + listed here. + +
+ )} +
+); + +export default ComplianceDashboard; diff --git a/packages/react-components/src/templates/TeamProfileHeader.tsx b/packages/react-components/src/templates/TeamProfileHeader.tsx index 60f930d6d3..1be812b5cc 100644 --- a/packages/react-components/src/templates/TeamProfileHeader.tsx +++ b/packages/react-components/src/templates/TeamProfileHeader.tsx @@ -1,3 +1,4 @@ +import { isEnabled } from '@asap-hub/flags'; import { TeamResponse, TeamTool } from '@asap-hub/model'; import { ResearchOutputPermissionsContext } from '@asap-hub/react-context'; import { network } from '@asap-hub/routing'; @@ -130,6 +131,8 @@ type TeamProfileHeaderProps = Readonly> & { readonly teamOutputsCount?: number; readonly pastEventsCount?: number; readonly teamDraftOutputsCount?: number; + readonly isAsapTeam?: boolean; + readonly manuscriptsCount?: number; }; const TeamProfileHeader: React.FC = ({ @@ -146,6 +149,8 @@ const TeamProfileHeader: React.FC = ({ pastEventsCount, teamDraftOutputsCount, isStaff, + manuscriptsCount, + isAsapTeam = false, }) => { const route = network({}).teams({}).team({ teamId: id }); const { canShareResearchOutput } = useContext( @@ -251,7 +256,11 @@ const TeamProfileHeader: React.FC = ({ {(tools || isStaff) && ( Team Workspace )} - + {isAsapTeam && isStaff && isEnabled('DISPLAY_MANUSCRIPTS') && ( + + Compliance ({manuscriptsCount}) + + )} Outputs ({teamOutputsCount}) diff --git a/packages/react-components/src/templates/__tests__/ComplianceDashboard.test.tsx b/packages/react-components/src/templates/__tests__/ComplianceDashboard.test.tsx new file mode 100644 index 0000000000..55729399b5 --- /dev/null +++ b/packages/react-components/src/templates/__tests__/ComplianceDashboard.test.tsx @@ -0,0 +1,45 @@ +import { complianceInitialSortingDirection } from '@asap-hub/model'; +import { render } from '@testing-library/react'; +import { ComponentProps } from 'react'; +import { ComplianceDashboard } from '..'; + +describe('ComplianceDashboard', () => { + const props: ComponentProps = { + data: [ + { + id: '1', + lastUpdated: '2023-01-01T08:00:00Z', + requestingApcCoverage: 'Yes', + status: 'Compliant', + team: { id: 'team-1', displayName: 'Test Team' }, + assignedUsers: [], + }, + ], + numberOfPages: 1, + currentPageIndex: 0, + renderPageHref: () => '', + sort: 'team_asc', + setSort: jest.fn(), + sortingDirection: complianceInitialSortingDirection, + setSortingDirection: jest.fn(), + }; + + it('renders the manuscript status card', () => { + const { getByText } = render(); + + expect(getByText('Manuscripts by status:')).toBeInTheDocument(); + expect(getByText('Waiting for Report')).toBeInTheDocument(); + }); + + it('renders the empty manuscript view when there are no manuscripts', () => { + const { getByText } = render(); + + expect(getByText('No manuscripts available.')).toBeInTheDocument(); + }); + + it('renders compliance table when there are manuscripts', () => { + const { getByText } = render(); + + expect(getByText('Test Team')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx index 46d566cc38..1b9f003086 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx @@ -1,4 +1,5 @@ import { createTeamResponseMembers } from '@asap-hub/fixtures'; +import { enable } from '@asap-hub/flags'; import { TeamRole } from '@asap-hub/model'; import { ResearchOutputPermissionsContext } from '@asap-hub/react-context'; import { fireEvent } from '@testing-library/dom'; @@ -154,6 +155,28 @@ it('renders workspace tabs when tools provided', () => { ]); }); +it('renders compliance tabs when is ASAP team and is staff', () => { + enable('DISPLAY_MANUSCRIPTS'); + render( + , + ); + expect( + screen.getAllByRole('link').map(({ textContent }) => textContent), + ).toEqual([ + 'About', + 'Team Workspace', + 'Compliance (0)', + 'Outputs (0)', + 'Upcoming Events (0)', + 'Past Events (0)', + ]); +}); + describe('Share an output button dropdown', () => { const renderWithPermissionsContext = ( canShareResearchOutput: boolean = true, diff --git a/packages/react-components/src/templates/index.ts b/packages/react-components/src/templates/index.ts index 3c81a14112..6c4cf845e8 100644 --- a/packages/react-components/src/templates/index.ts +++ b/packages/react-components/src/templates/index.ts @@ -9,6 +9,7 @@ export { default as AnalyticsEngagementPageBody } from './AnalyticsEngagementPag export { default as AnalyticsPageHeader } from './AnalyticsPageHeader'; export { default as BasicLayout } from './BasicLayout'; export { default as BiographyModal } from './BiographyModal'; +export { default as ComplianceDashboard } from './ComplianceDashboard'; export { default as ComplianceReportForm } from './ComplianceReportForm'; export { default as ContactInfoModal } from './ContactInfoModal'; export { default as ContentPage } from './ContentPage'; diff --git a/packages/routing/src/network.ts b/packages/routing/src/network.ts index 1ce52bf583..61a12efc81 100644 --- a/packages/routing/src/network.ts +++ b/packages/routing/src/network.ts @@ -119,6 +119,9 @@ const team = (() => { createComplianceReport, }, ); + + const compliance = route('/compliance', {}, {}); + const createOutput = route( '/create-output/:outputDocumentType', { outputDocumentType: outputDocumentTypeParser }, @@ -135,6 +138,7 @@ const team = (() => { { about, workspace, + compliance, outputs, createOutput, duplicateOutput,