Skip to content

Commit

Permalink
[ASAP-542] - submit compliance report (#4407)
Browse files Browse the repository at this point in the history
* add compliance report model to cms

* fix types

* backend updates

* frontend updates

* add tests

* update fixture

* update getManuscriptCreateDataObject

* update tests

* update schema

* add more tests for code coverage

* update tests

* update props for ManuscriptCard

* minor changes

* updates from code review

* update test to wait for enabled share button

* allow validation onBlur

* conditional onBlur for older forms
  • Loading branch information
AkosuaA authored Oct 16, 2024
1 parent e326273 commit 346fed8
Show file tree
Hide file tree
Showing 56 changed files with 2,014 additions and 28 deletions.
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

0 comments on commit 346fed8

Please sign in to comment.