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

[ASAP-542] - submit compliance report #4407

Merged
merged 17 commits into from
Oct 16, 2024
22 changes: 13 additions & 9 deletions apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Toast } from '@asap-hub/react-components';
import React, { createContext, useState } from 'react';

type FormType = 'manuscript' | 'compliance-report' | '';

type ManuscriptToastContextData = {
setShowSuccessBanner: React.Dispatch<React.SetStateAction<boolean>>;
setFormType: React.Dispatch<React.SetStateAction<FormType>>;
};

export const ManuscriptToastContext = createContext<ManuscriptToastContextData>(
Expand All @@ -14,17 +16,19 @@ export const ManuscriptToastProvider = ({
}: {
children: React.ReactNode;
}) => {
const [showSuccessBanner, setShowSuccessBanner] = useState<boolean>(false);
const [formType, setFormType] = useState<FormType>('');

const formTypeMapping = {
manuscript: 'Manuscript',
'compliance-report': 'Compliance Report',
};

return (
<ManuscriptToastContext.Provider value={{ setShowSuccessBanner }}>
<ManuscriptToastContext.Provider value={{ setFormType }}>
<>
{showSuccessBanner && (
<Toast
accent="successLarge"
onClose={() => setShowSuccessBanner(false)}
>
Manuscript submitted successfully.
{!!formType && (
<Toast accent="successLarge" onClose={() => setFormType('')}>
{formTypeMapping[formType]} submitted successfully.
</Toast>
)}
{children}
Expand Down
60 changes: 60 additions & 0 deletions apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Frame } from '@asap-hub/frontend-utils';
import {
ComplianceReportForm,
ComplianceReportHeader,
NotFoundPage,
usePushFromHere,
} from '@asap-hub/react-components';
import { network } from '@asap-hub/routing';
import { FormProvider, useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import {
refreshTeamState,
useManuscriptById,
usePostComplianceReport,
} from './state';
import { useManuscriptToast } from './useManuscriptToast';

type TeamComplianceReportProps = {
teamId: string;
};
const TeamComplianceReport: React.FC<TeamComplianceReportProps> = ({
teamId,
}) => {
const { manuscriptId } = useParams<{ manuscriptId: string }>();
const manuscript = useManuscriptById(manuscriptId);
const { setFormType } = useManuscriptToast();

const pushFromHere = usePushFromHere();

const setRefreshTeamState = useSetRecoilState(refreshTeamState(teamId));
const form = useForm();
const createComplianceReport = usePostComplianceReport();

if (manuscript && manuscript.versions[0]) {
const onSuccess = () => {
const path = network({}).teams({}).team({ teamId }).workspace({}).$;
setFormType('compliance-report');
setRefreshTeamState((value) => value + 1);
pushFromHere(path);
};

return (
<FormProvider {...form}>
<Frame title="Create Compliance Report">
<ComplianceReportHeader />
<ComplianceReportForm
onSuccess={onSuccess}
onSave={createComplianceReport}
manuscriptTitle={manuscript.title}
manuscriptVersionId={manuscript.versions[0].id}
/>
</Frame>
</FormProvider>
);
}

return <NotFoundPage />;
};
export default TeamComplianceReport;
4 changes: 2 additions & 2 deletions apps/crn-frontend/src/network/teams/TeamManuscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const TeamManuscript: React.FC<TeamManuscriptProps> = ({ teamId }) => {
const team = useTeamById(teamId);

const { eligibilityReasons } = useEligibilityReason();
const { setShowSuccessBanner } = useManuscriptToast();
const { setFormType } = useManuscriptToast();
const form = useForm();
const createManuscript = usePostManuscript();
const handleFileUpload = useUploadManuscriptFile();
Expand All @@ -42,7 +42,7 @@ const TeamManuscript: React.FC<TeamManuscriptProps> = ({ teamId }) => {

const onSuccess = () => {
const path = network({}).teams({}).team({ teamId }).workspace({}).$;
setShowSuccessBanner(true);
setFormType('manuscript');
setRefreshTeamState((value) => value + 1);
pushFromHere(path);
};
Expand Down
18 changes: 16 additions & 2 deletions apps/crn-frontend/src/network/teams/TeamProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { useUpcomingAndPastEvents } from '../events';
import ProfileSwitch from '../ProfileSwitch';

import { ManuscriptToastProvider } from './ManuscriptToastProvider';
import { useTeamById } from './state';
import { useCanShareComplianceReport, useTeamById } from './state';
import TeamManuscript from './TeamManuscript';
import { EligibilityReasonProvider } from './EligibilityReasonProvider';
import TeamComplianceReport from './TeamComplianceReport';

const loadAbout = () =>
import(/* webpackChunkName: "network-team-about" */ './About');
Expand Down Expand Up @@ -68,7 +69,6 @@ const TeamProfile: FC<TeamProfileProps> = ({ currentTime }) => {
const { path } = useRouteMatch();
const route = network({}).teams({}).team;
const [teamListElementId] = useState(`team-list-${uuid()}`);

const { teamId } = useRouteParams(route);
const team = useTeamById(teamId);

Expand All @@ -89,6 +89,8 @@ const TeamProfile: FC<TeamProfileProps> = ({ currentTime }) => {
const canDuplicateResearchOutput = useCanDuplicateResearchOutput('teams', [
teamId,
]);

const canCreateComplianceReport = useCanShareComplianceReport();
const [upcomingEvents, pastEvents] = useUpcomingAndPastEvents(currentTime, {
teamId,
});
Expand Down Expand Up @@ -147,6 +149,18 @@ const TeamProfile: FC<TeamProfileProps> = ({ currentTime }) => {
<TeamManuscript teamId={teamId} />
</Frame>
</Route>
{canCreateComplianceReport && (
<Route
path={
workspace({}).$ +
workspace({}).createComplianceReport.template
}
>
<Frame title="Create Compliance Report">
<TeamComplianceReport teamId={teamId} />
</Frame>
</Route>
)}
{canShareResearchOutput && (
<Route path={path + createOutput.template}>
<Frame title="Share Output">
Expand Down
4 changes: 3 additions & 1 deletion apps/crn-frontend/src/network/teams/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { TeamTool, TeamResponse } from '@asap-hub/model';
import { network, useRouteParams } from '@asap-hub/routing';
import { ToastContext } from '@asap-hub/react-context';

import { usePatchTeamById } from './state';
import { useCanShareComplianceReport, usePatchTeamById } from './state';
import { useEligibilityReason } from './useEligibilityReason';

interface WorkspaceProps {
Expand All @@ -19,6 +19,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ team }) => {
const route = network({}).teams({}).team({ teamId: team.id }).workspace({});
const { path } = useRouteMatch();
const { setEligibilityReasons } = useEligibilityReason();
const canShareComplianceReport = useCanShareComplianceReport();

const [deleting, setDeleting] = useState(false);
const patchTeam = usePatchTeamById(team.id);
Expand Down Expand Up @@ -50,6 +51,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ team }) => {
setDeleting(false);
}
}
canShareComplianceReport={canShareComplianceReport}
/>
</Route>
<Route exact path={path + route.tools.template}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
Auth0Provider,
WhenReady,
} from '@asap-hub/crn-frontend/src/auth/test-utils';
import { network } from '@asap-hub/routing';
import {
render,
screen,
waitFor,
waitForElementToBeRemoved,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMemoryHistory } from 'history';
import { ComponentProps, Suspense } from 'react';
import { Route, Router } from 'react-router-dom';
import { RecoilRoot } from 'recoil';

import { createComplianceReport, getManuscript } from '../api';
import { ManuscriptToastProvider } from '../ManuscriptToastProvider';
import { refreshTeamState } from '../state';
import TeamComplianceReport from '../TeamComplianceReport';

const manuscriptResponse = {
id: 'manuscript-1',
title: 'The Manuscript',
versions: [{ id: 'manuscript-version-1' }],
};
const complianceReportResponse = { id: 'compliance-report-1' };

const teamId = '42';

jest.mock('../api', () => ({
createComplianceReport: jest.fn().mockResolvedValue(complianceReportResponse),
getManuscript: jest.fn().mockResolvedValue(manuscriptResponse),
}));

const mockGetManuscript = getManuscript as jest.MockedFunction<
typeof getManuscript
>;

beforeEach(() => {
jest.resetModules();
});

const renderPage = async (
user: ComponentProps<typeof Auth0Provider>['user'] = {},
history = createMemoryHistory({
initialEntries: [
network({})
.teams({})
.team({ teamId })
.workspace({})
.createComplianceReport({ manuscriptId: manuscriptResponse.id }).$,
],
}),
) => {
const path =
network.template +
network({}).teams.template +
network({}).teams({}).team.template +
network({}).teams({}).team({ teamId }).workspace.template +
network({}).teams({}).team({ teamId }).workspace({}).createComplianceReport
.template;

const { container } = render(
<RecoilRoot
initializeState={({ set }) => {
set(refreshTeamState(teamId), Math.random());
}}
>
<Suspense fallback="loading">
<Auth0Provider user={user}>
<WhenReady>
<Router history={history}>
<Route path={path}>
<ManuscriptToastProvider>
<TeamComplianceReport teamId={teamId} />
</ManuscriptToastProvider>
</Route>
</Router>
</WhenReady>
</Auth0Provider>
</Suspense>
</RecoilRoot>,
);
await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));
return { container };
};

it('renders compliance report form page', async () => {
const { container } = await renderPage();

expect(container).toHaveTextContent(
'Share the compliance report associated with this manuscript.',
);
expect(container).toHaveTextContent('Title of Manuscript');
});

it('can publish a form when the data is valid and navigates to team workspace', async () => {
const url = 'https://compliancereport.com';
const description = 'compliance report description';
const history = createMemoryHistory({
initialEntries: [
network({})
.teams({})
.team({ teamId })
.workspace({})
.createComplianceReport({ manuscriptId: manuscriptResponse.id }).$,
],
});

await renderPage({}, history);

userEvent.type(screen.getByRole('textbox', { name: /url/i }), url);

userEvent.type(
screen.getByRole('textbox', {
name: /Compliance Report Description/i,
}),
description,
);

const shareButton = screen.getByRole('button', { name: /Share/i });
await waitFor(() => expect(shareButton).toBeEnabled());

userEvent.click(shareButton);

await waitFor(() => {
expect(createComplianceReport).toHaveBeenCalledWith(
{
url,
description,
manuscriptVersionId: manuscriptResponse.versions[0]!.id,
},
expect.anything(),
);
expect(history.location.pathname).toBe(
`/network/teams/${teamId}/workspace`,
);
});
});

it('renders not found when the manuscript hook does not return a manuscript with a version', async () => {
mockGetManuscript.mockResolvedValue(undefined);
await renderPage();

expect(screen.getByRole('heading').textContent).toContain(
'Sorry! We can’t seem to find that page.',
);
});
Loading
Loading