diff --git a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx index cb5795137f..34e2bcbdef 100644 --- a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx +++ b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx @@ -6,13 +6,16 @@ import { } from '@asap-hub/react-components'; import { network } from '@asap-hub/routing'; import { FormProvider, useForm } from 'react-hook-form'; -import { usePostManuscript } from './state'; +import { useSetRecoilState } from 'recoil'; +import { refreshTeamState, usePostManuscript } from './state'; import { useManuscriptToast } from './useManuscriptToast'; type TeamManuscriptProps = { teamId: string; }; const TeamManuscript: React.FC = ({ teamId }) => { + const setRefreshTeamState = useSetRecoilState(refreshTeamState(teamId)); + const { setShowSuccessBanner } = useManuscriptToast(); const form = useForm(); const createManuscript = usePostManuscript(); @@ -21,8 +24,8 @@ const TeamManuscript: React.FC = ({ teamId }) => { const onSuccess = () => { const path = network({}).teams({}).team({ teamId }).workspace({}).$; - setShowSuccessBanner(true); + setRefreshTeamState((value) => value + 1); pushFromHere(path); }; @@ -30,7 +33,11 @@ const TeamManuscript: React.FC = ({ teamId }) => { - + ); diff --git a/apps/crn-frontend/src/network/teams/__tests__/TeamManuscript.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/TeamManuscript.test.tsx index df6f6b213a..a4ca4b020d 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/TeamManuscript.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamManuscript.test.tsx @@ -96,7 +96,10 @@ it('can publish a form when the data is valid and navigates to team workspace', userEvent.click(submitButton); await waitFor(() => { - expect(createManuscript).toHaveBeenCalledWith({ title }, expect.anything()); + expect(createManuscript).toHaveBeenCalledWith( + { title, teamId }, + expect.anything(), + ); expect(history.location.pathname).toBe( `/network/teams/${teamId}/workspace`, ); diff --git a/apps/crn-frontend/src/network/teams/__tests__/api.test.ts b/apps/crn-frontend/src/network/teams/__tests__/api.test.ts index 64e5322e32..6038f55551 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/api.test.ts +++ b/apps/crn-frontend/src/network/teams/__tests__/api.test.ts @@ -255,6 +255,7 @@ describe('Manuscript', () => { describe('POST', () => { const payload: ManuscriptPostRequest = { title: 'The Manuscript', + teamId: '42', }; it('makes an authorized POST request to create a manuscript', async () => { nock(API_BASE_URL, { reqheaders: { authorization: 'Bearer x' } }) diff --git a/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts b/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts index 9a02cfd0bb..36fb87ab58 100644 --- a/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts +++ b/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts @@ -4,6 +4,7 @@ import { FetchManuscriptByIdQuery, FetchManuscriptByIdQueryVariables, FETCH_MANUSCRIPT_BY_ID, + getLinkEntities, GraphQLClient, } from '@asap-hub/contentful'; import { @@ -44,8 +45,13 @@ export class ManuscriptContentfulDataProvider async create(input: ManuscriptCreateDataObject): Promise { const environment = await this.getRestClient(); + const { teamId, ...plainFields } = input; + const manuscriptEntry = await environment.createEntry('manuscripts', { - fields: addLocaleToFields(input), + fields: addLocaleToFields({ + ...plainFields, + teams: getLinkEntities([teamId]), + }), }); await manuscriptEntry.publish(); @@ -59,4 +65,5 @@ const parseGraphQLManuscript = ( ): ManuscriptDataObject => ({ id: manuscripts.sys.id, title: manuscripts.title || '', + teamId: manuscripts.teamsCollection?.items[0]?.sys.id || '', }); diff --git a/apps/crn-server/src/data-providers/contentful/team.data-provider.ts b/apps/crn-server/src/data-providers/contentful/team.data-provider.ts index e8a3b61f50..4e723e95e2 100644 --- a/apps/crn-server/src/data-providers/contentful/team.data-provider.ts +++ b/apps/crn-server/src/data-providers/contentful/team.data-provider.ts @@ -7,6 +7,7 @@ import { TeamListItemDataObject, ListTeamDataObject, TeamRole, + TeamManuscript, } from '@asap-hub/model'; import { @@ -22,7 +23,7 @@ import { Environment, addLocaleToFields, } from '@asap-hub/contentful'; -import { parseUserDisplayName } from '@asap-hub/server-common'; +import { cleanArray, parseUserDisplayName } from '@asap-hub/server-common'; import { sortMembers } from '../transformers'; import { @@ -300,6 +301,10 @@ export const parseContentfulGraphQlTeam = ( lastModifiedDate: new Date(item.sys.publishedAt).toISOString(), tags: parseResearchTags(item.researchTagsCollection?.items || []), tools, + manuscripts: cleanArray(item.linkedFrom?.manuscriptsCollection?.items).map( + (manuscript) => + ({ id: manuscript.sys.id, title: manuscript.title }) as TeamManuscript, + ), projectSummary: item.projectSummary ?? undefined, members: members.sort(sortMembers), labCount, diff --git a/apps/crn-server/src/routes/manuscript.route.ts b/apps/crn-server/src/routes/manuscript.route.ts index c1b4d48922..684180897b 100644 --- a/apps/crn-server/src/routes/manuscript.route.ts +++ b/apps/crn-server/src/routes/manuscript.route.ts @@ -32,7 +32,11 @@ export const manuscriptRouteFactory = ( const { body, loggedInUser } = req; const createRequest = validateManuscriptPostRequestParameters(body); - if (!loggedInUser) throw Boom.forbidden(); + const userBelongsToTeam = loggedInUser?.teams.some( + (team) => team.id === createRequest.teamId, + ); + + if (!loggedInUser || !userBelongsToTeam) throw Boom.forbidden(); const manuscript = await manuscriptController.create(createRequest); diff --git a/apps/crn-server/test/data-providers/contentful/manuscript.data-provider.test.ts b/apps/crn-server/test/data-providers/contentful/manuscript.data-provider.test.ts index 404616bdfc..1591fe87aa 100644 --- a/apps/crn-server/test/data-providers/contentful/manuscript.data-provider.test.ts +++ b/apps/crn-server/test/data-providers/contentful/manuscript.data-provider.test.ts @@ -96,6 +96,17 @@ describe('Manuscripts Contentful Data Provider', () => { title: { 'en-US': 'Manuscript Title', }, + teams: { + 'en-US': [ + { + sys: { + id: 'team-1', + linkType: 'Entry', + type: 'Link', + }, + }, + ], + }, }, }); expect(publish).toHaveBeenCalled(); diff --git a/apps/crn-server/test/fixtures/manuscript.fixtures.ts b/apps/crn-server/test/fixtures/manuscript.fixtures.ts index 70cb62cf8e..9762697818 100644 --- a/apps/crn-server/test/fixtures/manuscript.fixtures.ts +++ b/apps/crn-server/test/fixtures/manuscript.fixtures.ts @@ -10,6 +10,7 @@ export const getManuscriptDataObject = ( ): ManuscriptDataObject => ({ id: 'manuscript-id-1', title: 'Manuscript Title', + teamId: 'team-1', ...data, }); @@ -26,12 +27,14 @@ export const getContentfulGraphqlManuscripts = ( id: 'manuscript-id-1', }, title: 'Manuscript Title', - + teamsCollection: { + items: [{ sys: { id: 'team-1' } }], + }, ...props, }); export const getManuscriptCreateDataObject = (): ManuscriptCreateDataObject => { - const { title } = getManuscriptDataObject(); + const { title, teamId } = getManuscriptDataObject(); - return { title }; + return { title, teamId }; }; diff --git a/apps/crn-server/test/fixtures/teams.fixtures.ts b/apps/crn-server/test/fixtures/teams.fixtures.ts index 3e3a5f3581..9ed2de0737 100644 --- a/apps/crn-server/test/fixtures/teams.fixtures.ts +++ b/apps/crn-server/test/fixtures/teams.fixtures.ts @@ -24,6 +24,7 @@ export const getContentfulGraphql = (teamById = false) => ({ TeamMembershipCollection: () => getContentfulGraphqlTeamMemberships(), Users: () => getContentfulGraphqlTeamMembers(), UsersLabsCollection: () => getContentfulGraphqlTeamMemberLabs(), + ManuscriptsCollection: () => getContentfulGraphqlManuscripts(), }); export const getContentfulGraphqlTeamById = (): NonNullable< @@ -50,6 +51,7 @@ export const getContentfulGraphqlTeamById = (): NonNullable< }, }, linkedFrom: { + manuscriptsCollection: getContentfulGraphqlManuscripts(), teamMembershipCollection: { items: [ { @@ -135,6 +137,15 @@ export const getContentfulGraphqlTeamMemberLabs = () => ({ ], }); +export const getContentfulGraphqlManuscripts = (): NonNullable< + NonNullable['linkedFrom'] +>['manuscriptsCollection'] => ({ + items: [ + { sys: { id: '1' }, title: 'Manuscript 1' }, + { sys: { id: '2' }, title: 'Manuscript 2' }, + ], +}); + export const getContentfulTeamsGraphqlResponse = (): ContentfulFetchTeamsQuery => ({ teamsCollection: { @@ -175,6 +186,10 @@ export const getTeamDataObject = (): TeamDataObject => ({ ], }, ], + manuscripts: [ + { id: '1', title: 'Manuscript 1' }, + { id: '2', title: 'Manuscript 2' }, + ], projectTitle: 'The genome-microbiome axis in the cause of Parkinson disease: Mechanistic insights and therapeutic implications from experimental models and a genetically stratified patient population.', proposalURL: '4cfb1b7b-bafe-4fca-b2ab-197e84d98996', diff --git a/apps/crn-server/test/routes/manuscript.route.test.ts b/apps/crn-server/test/routes/manuscript.route.test.ts index c2622769db..6e4daf3778 100644 --- a/apps/crn-server/test/routes/manuscript.route.test.ts +++ b/apps/crn-server/test/routes/manuscript.route.test.ts @@ -1,5 +1,5 @@ import { createUserResponse } from '@asap-hub/fixtures'; -import { UserResponse } from '@asap-hub/model'; +import { ManuscriptPostRequest, UserResponse } from '@asap-hub/model'; import { AuthHandler } from '@asap-hub/server-common'; import Boom from '@hapi/boom'; import supertest from 'supertest'; @@ -79,7 +79,7 @@ describe('/manuscripts/ route', () => { describe('POST /manuscripts/', () => { const manuscriptResponse = getManuscriptResponse(); - test('Should return 403 when not allowed to create a manuscript', async () => { + test('Should return 403 when not allowed to create a manuscript because user is not onboarded', async () => { const createManuscriptRequest = getManuscriptCreateDataObject(); userMockFactory.mockReturnValueOnce({ @@ -95,8 +95,54 @@ describe('/manuscripts/ route', () => { expect(response.status).toEqual(403); }); + test('Should return 403 when not allowed to create a manuscript because user does not belong to the team', async () => { + const createManuscriptRequest: ManuscriptPostRequest = { + ...getManuscriptCreateDataObject(), + teamId: 'team-3', + }; + + userMockFactory.mockReturnValueOnce({ + ...createUserResponse(), + teams: [ + { + role: 'Key Personnel', + displayName: 'Test 1', + id: 'test-1', + }, + { + role: 'Collaborating PI', + displayName: 'Test 2', + id: 'test-2', + }, + ], + }); + + const response = await supertest(app) + .post('/manuscripts') + .send(createManuscriptRequest) + .set('Accept', 'application/json'); + + expect(response.status).toEqual(403); + }); + test('Should return a 201 when is hit', async () => { - const createManuscriptRequest = getManuscriptCreateDataObject(); + const teamId = 'team-1'; + + const createManuscriptRequest: ManuscriptPostRequest = { + ...getManuscriptCreateDataObject(), + teamId, + }; + + userMockFactory.mockReturnValueOnce({ + ...createUserResponse(), + teams: [ + { + role: 'Key Personnel', + displayName: 'Test 1', + id: teamId, + }, + ], + }); manuscriptControllerMock.create.mockResolvedValueOnce(manuscriptResponse); diff --git a/apps/storybook/src/TeamProfilePage.stories.tsx b/apps/storybook/src/TeamProfilePage.stories.tsx index 8392f533cb..d4c61e8563 100644 --- a/apps/storybook/src/TeamProfilePage.stories.tsx +++ b/apps/storybook/src/TeamProfilePage.stories.tsx @@ -22,6 +22,7 @@ const props = (): Omit, 'children'> => ({ lastModifiedDate: formatISO(subDays(new Date(), 2)), labCount: number('Lab count', 15), tags: [], + manuscripts: [], teamListElementId: 'uuid', upcomingEventsCount: 7, pointOfContact: { diff --git a/packages/contentful/migrations/crn/manuscripts/20240515135710-add-teams-field.js b/packages/contentful/migrations/crn/manuscripts/20240515135710-add-teams-field.js new file mode 100644 index 0000000000..2c995cf6b4 --- /dev/null +++ b/packages/contentful/migrations/crn/manuscripts/20240515135710-add-teams-field.js @@ -0,0 +1,37 @@ +module.exports.description = 'Add field teams'; + +module.exports.up = (migration) => { + const manuscripts = migration.editContentType('manuscripts'); + + manuscripts + .createField('teams') + .name('Teams') + .type('Array') + .localized(false) + .required(false) + .validations([]) + .disabled(false) + .omitted(false) + .items({ + type: 'Link', + + validations: [ + { + linkContentType: ['teams'], + }, + ], + + linkType: 'Entry', + }); + + manuscripts.changeFieldControl('teams', 'builtin', 'entryLinksEditor', { + bulkEditing: false, + showLinkEntityAction: true, + showCreateEntityAction: false, + }); +}; + +module.exports.down = (migration) => { + const manuscripts = migration.editContentType('manuscripts'); + manuscripts.deleteField('announcements'); +}; diff --git a/packages/contentful/src/crn/autogenerated-gql/gql.ts b/packages/contentful/src/crn/autogenerated-gql/gql.ts index e5d9f6d703..941406dfa6 100644 --- a/packages/contentful/src/crn/autogenerated-gql/gql.ts +++ b/packages/contentful/src/crn/autogenerated-gql/gql.ts @@ -67,7 +67,7 @@ const documents = { types.FetchInterestGroupsByUserIdDocument, '\n query FetchLabs($limit: Int, $skip: Int, $where: LabsFilter) {\n labsCollection(limit: $limit, skip: $skip, where: $where, order: name_ASC) {\n total\n items {\n sys {\n id\n }\n name\n }\n }\n }\n': types.FetchLabsDocument, - '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n }\n': + '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n }\n }\n }\n': types.ManuscriptsContentFragmentDoc, '\n query FetchManuscriptById($id: String!) {\n manuscripts(id: $id) {\n ...ManuscriptsContent\n }\n }\n \n': types.FetchManuscriptByIdDocument, @@ -97,7 +97,7 @@ const documents = { types.FetchResearchTagsDocument, '\n query FetchResearchTagsById($id: String!) {\n researchTags(id: $id) {\n ...ResearchTagsContent\n }\n }\n \n': types.FetchResearchTagsByIdDocument, - '\n query FetchTeamById($id: String!) {\n teams(id: $id) {\n sys {\n id\n publishedAt\n }\n displayName\n inactiveSince\n projectSummary\n projectTitle\n proposal {\n sys {\n id\n }\n }\n toolsCollection {\n items {\n name\n description\n url\n }\n }\n researchTagsCollection(limit: 20) {\n items {\n sys {\n id\n }\n name\n }\n }\n linkedFrom {\n teamMembershipCollection(limit: 100) {\n items {\n role\n inactiveSinceDate\n linkedFrom {\n usersCollection(limit: 1) {\n items {\n sys {\n id\n }\n onboarded\n firstName\n nickname\n lastName\n email\n alumniSinceDate\n avatar {\n url\n }\n labsCollection(limit: 5) {\n items {\n sys {\n id\n }\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n': + '\n query FetchTeamById($id: String!) {\n teams(id: $id) {\n sys {\n id\n publishedAt\n }\n displayName\n inactiveSince\n projectSummary\n projectTitle\n proposal {\n sys {\n id\n }\n }\n toolsCollection {\n items {\n name\n description\n url\n }\n }\n researchTagsCollection(limit: 20) {\n items {\n sys {\n id\n }\n name\n }\n }\n linkedFrom {\n manuscriptsCollection(limit: 20, order: sys_firstPublishedAt_DESC) {\n items {\n sys {\n id\n }\n title\n }\n }\n teamMembershipCollection(limit: 100) {\n items {\n role\n inactiveSinceDate\n linkedFrom {\n usersCollection(limit: 1) {\n items {\n sys {\n id\n }\n onboarded\n firstName\n nickname\n lastName\n email\n alumniSinceDate\n avatar {\n url\n }\n labsCollection(limit: 5) {\n items {\n sys {\n id\n }\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n': types.FetchTeamByIdDocument, '\n query FetchTeams(\n $limit: Int\n $skip: Int\n $order: [TeamsOrder]\n $where: TeamsFilter\n ) {\n teamsCollection(limit: $limit, skip: $skip, order: $order, where: $where) {\n total\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n projectTitle\n researchTagsCollection(limit: 20) {\n items {\n sys {\n id\n }\n name\n }\n }\n linkedFrom {\n teamMembershipCollection(limit: 100) {\n items {\n role\n linkedFrom {\n usersCollection(limit: 1) {\n items {\n sys {\n id\n }\n onboarded\n labsCollection(limit: 5) {\n items {\n sys {\n id\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n': types.FetchTeamsDocument, @@ -307,8 +307,8 @@ export function gql( * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql( - source: '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n }\n', -): (typeof documents)['\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n }\n']; + source: '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n }\n }\n }\n', +): (typeof documents)['\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n }\n }\n }\n']; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -397,8 +397,8 @@ export function gql( * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql( - source: '\n query FetchTeamById($id: String!) {\n teams(id: $id) {\n sys {\n id\n publishedAt\n }\n displayName\n inactiveSince\n projectSummary\n projectTitle\n proposal {\n sys {\n id\n }\n }\n toolsCollection {\n items {\n name\n description\n url\n }\n }\n researchTagsCollection(limit: 20) {\n items {\n sys {\n id\n }\n name\n }\n }\n linkedFrom {\n teamMembershipCollection(limit: 100) {\n items {\n role\n inactiveSinceDate\n linkedFrom {\n usersCollection(limit: 1) {\n items {\n sys {\n id\n }\n onboarded\n firstName\n nickname\n lastName\n email\n alumniSinceDate\n avatar {\n url\n }\n labsCollection(limit: 5) {\n items {\n sys {\n id\n }\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n', -): (typeof documents)['\n query FetchTeamById($id: String!) {\n teams(id: $id) {\n sys {\n id\n publishedAt\n }\n displayName\n inactiveSince\n projectSummary\n projectTitle\n proposal {\n sys {\n id\n }\n }\n toolsCollection {\n items {\n name\n description\n url\n }\n }\n researchTagsCollection(limit: 20) {\n items {\n sys {\n id\n }\n name\n }\n }\n linkedFrom {\n teamMembershipCollection(limit: 100) {\n items {\n role\n inactiveSinceDate\n linkedFrom {\n usersCollection(limit: 1) {\n items {\n sys {\n id\n }\n onboarded\n firstName\n nickname\n lastName\n email\n alumniSinceDate\n avatar {\n url\n }\n labsCollection(limit: 5) {\n items {\n sys {\n id\n }\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n']; + source: '\n query FetchTeamById($id: String!) {\n teams(id: $id) {\n sys {\n id\n publishedAt\n }\n displayName\n inactiveSince\n projectSummary\n projectTitle\n proposal {\n sys {\n id\n }\n }\n toolsCollection {\n items {\n name\n description\n url\n }\n }\n researchTagsCollection(limit: 20) {\n items {\n sys {\n id\n }\n name\n }\n }\n linkedFrom {\n manuscriptsCollection(limit: 20, order: sys_firstPublishedAt_DESC) {\n items {\n sys {\n id\n }\n title\n }\n }\n teamMembershipCollection(limit: 100) {\n items {\n role\n inactiveSinceDate\n linkedFrom {\n usersCollection(limit: 1) {\n items {\n sys {\n id\n }\n onboarded\n firstName\n nickname\n lastName\n email\n alumniSinceDate\n avatar {\n url\n }\n labsCollection(limit: 5) {\n items {\n sys {\n id\n }\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n', +): (typeof documents)['\n query FetchTeamById($id: String!) {\n teams(id: $id) {\n sys {\n id\n publishedAt\n }\n displayName\n inactiveSince\n projectSummary\n projectTitle\n proposal {\n sys {\n id\n }\n }\n toolsCollection {\n items {\n name\n description\n url\n }\n }\n researchTagsCollection(limit: 20) {\n items {\n sys {\n id\n }\n name\n }\n }\n linkedFrom {\n manuscriptsCollection(limit: 20, order: sys_firstPublishedAt_DESC) {\n items {\n sys {\n id\n }\n title\n }\n }\n teamMembershipCollection(limit: 100) {\n items {\n role\n inactiveSinceDate\n linkedFrom {\n usersCollection(limit: 1) {\n items {\n sys {\n id\n }\n onboarded\n firstName\n nickname\n lastName\n email\n alumniSinceDate\n avatar {\n url\n }\n labsCollection(limit: 5) {\n items {\n sys {\n id\n }\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n']; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/contentful/src/crn/autogenerated-gql/graphql.ts b/packages/contentful/src/crn/autogenerated-gql/graphql.ts index f318b2b9dd..04f0951205 100644 --- a/packages/contentful/src/crn/autogenerated-gql/graphql.ts +++ b/packages/contentful/src/crn/autogenerated-gql/graphql.ts @@ -3392,6 +3392,7 @@ export type Manuscripts = Entry & { contentfulMetadata: ContentfulMetadata; linkedFrom?: Maybe; sys: Sys; + teamsCollection?: Maybe; title?: Maybe; }; @@ -3400,6 +3401,16 @@ export type ManuscriptsLinkedFromArgs = { allowedLocales?: InputMaybe>>; }; +/** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/manuscripts) */ +export type ManuscriptsTeamsCollectionArgs = { + limit?: InputMaybe; + locale?: InputMaybe; + order?: InputMaybe>>; + preview?: InputMaybe; + skip?: InputMaybe; + where?: InputMaybe; +}; + /** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/manuscripts) */ export type ManuscriptsTitleArgs = { locale?: InputMaybe; @@ -3417,6 +3428,8 @@ export type ManuscriptsFilter = { OR?: InputMaybe>>; contentfulMetadata?: InputMaybe; sys?: InputMaybe; + teams?: InputMaybe; + teamsCollection_exists?: InputMaybe; title?: InputMaybe; title_contains?: InputMaybe; title_exists?: InputMaybe; @@ -3450,6 +3463,30 @@ export enum ManuscriptsOrder { TitleDesc = 'title_DESC', } +export type ManuscriptsTeamsCollection = { + items: Array>; + limit: Scalars['Int']; + skip: Scalars['Int']; + total: Scalars['Int']; +}; + +export enum ManuscriptsTeamsCollectionOrder { + ApplicationNumberAsc = 'applicationNumber_ASC', + ApplicationNumberDesc = 'applicationNumber_DESC', + DisplayNameAsc = 'displayName_ASC', + DisplayNameDesc = 'displayName_DESC', + InactiveSinceAsc = 'inactiveSince_ASC', + InactiveSinceDesc = 'inactiveSince_DESC', + SysFirstPublishedAtAsc = 'sys_firstPublishedAt_ASC', + SysFirstPublishedAtDesc = 'sys_firstPublishedAt_DESC', + SysIdAsc = 'sys_id_ASC', + SysIdDesc = 'sys_id_DESC', + SysPublishedAtAsc = 'sys_publishedAt_ASC', + SysPublishedAtDesc = 'sys_publishedAt_DESC', + SysPublishedVersionAsc = 'sys_publishedVersion_ASC', + SysPublishedVersionDesc = 'sys_publishedVersion_DESC', +} + /** Videos and PDFs [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/media) */ export type Media = Entry & { contentfulMetadata: ContentfulMetadata; @@ -6563,6 +6600,7 @@ export type TeamsLinkingCollections = { entryCollection?: Maybe; eventSpeakersCollection?: Maybe; interestGroupsCollection?: Maybe; + manuscriptsCollection?: Maybe; researchOutputsCollection?: Maybe; teamMembershipCollection?: Maybe; tutorialsCollection?: Maybe; @@ -6605,6 +6643,16 @@ export type TeamsLinkingCollectionsInterestGroupsCollectionArgs = { skip?: InputMaybe; }; +export type TeamsLinkingCollectionsManuscriptsCollectionArgs = { + limit?: InputMaybe; + locale?: InputMaybe; + order?: InputMaybe< + Array> + >; + preview?: InputMaybe; + skip?: InputMaybe; +}; + export type TeamsLinkingCollectionsResearchOutputsCollectionArgs = { limit?: InputMaybe; locale?: InputMaybe; @@ -6678,6 +6726,19 @@ export enum TeamsLinkingCollectionsInterestGroupsCollectionOrder { SysPublishedVersionDesc = 'sys_publishedVersion_DESC', } +export enum TeamsLinkingCollectionsManuscriptsCollectionOrder { + SysFirstPublishedAtAsc = 'sys_firstPublishedAt_ASC', + SysFirstPublishedAtDesc = 'sys_firstPublishedAt_DESC', + SysIdAsc = 'sys_id_ASC', + SysIdDesc = 'sys_id_DESC', + SysPublishedAtAsc = 'sys_publishedAt_ASC', + SysPublishedAtDesc = 'sys_publishedAt_DESC', + SysPublishedVersionAsc = 'sys_publishedVersion_ASC', + SysPublishedVersionDesc = 'sys_publishedVersion_DESC', + TitleAsc = 'title_ASC', + TitleDesc = 'title_DESC', +} + export enum TeamsLinkingCollectionsResearchOutputsCollectionOrder { AccessionAsc = 'accession_ASC', AccessionDesc = 'accession_DESC', @@ -14330,6 +14391,7 @@ export type FetchLabsQuery = { export type ManuscriptsContentFragment = Pick & { sys: Pick; + teamsCollection?: Maybe<{ items: Array }>> }>; }; export type FetchManuscriptByIdQueryVariables = Exact<{ @@ -14337,7 +14399,14 @@ export type FetchManuscriptByIdQueryVariables = Exact<{ }>; export type FetchManuscriptByIdQuery = { - manuscripts?: Maybe & { sys: Pick }>; + manuscripts?: Maybe< + Pick & { + sys: Pick; + teamsCollection?: Maybe<{ + items: Array }>>; + }>; + } + >; }; export type NewsContentFragment = Pick< @@ -15794,6 +15863,11 @@ export type FetchTeamByIdQuery = { >; }>; linkedFrom?: Maybe<{ + manuscriptsCollection?: Maybe<{ + items: Array< + Maybe & { sys: Pick }> + >; + }>; teamMembershipCollection?: Maybe<{ items: Array< Maybe< @@ -19129,6 +19203,44 @@ export const ManuscriptsContentFragmentDoc = { }, }, { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'teamsCollection' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'limit' }, + value: { kind: 'IntValue', value: '10' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'items' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'sys' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'id' }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, ], }, }, @@ -28621,6 +28733,56 @@ export const FetchTeamByIdDocument = { selectionSet: { kind: 'SelectionSet', selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'manuscriptsCollection' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'limit' }, + value: { kind: 'IntValue', value: '20' }, + }, + { + kind: 'Argument', + name: { kind: 'Name', value: 'order' }, + value: { + kind: 'EnumValue', + value: 'sys_firstPublishedAt_DESC', + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'items' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'sys' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'id' }, + }, + ], + }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'title' }, + }, + ], + }, + }, + ], + }, + }, { kind: 'Field', name: { diff --git a/packages/contentful/src/crn/queries/manuscript.queries.ts b/packages/contentful/src/crn/queries/manuscript.queries.ts index f72eb7d7f6..f0e6b074b7 100644 --- a/packages/contentful/src/crn/queries/manuscript.queries.ts +++ b/packages/contentful/src/crn/queries/manuscript.queries.ts @@ -8,6 +8,13 @@ export const manuscriptContentQueryFragment = gql` id } title + teamsCollection(limit: 10) { + items { + sys { + id + } + } + } } `; diff --git a/packages/contentful/src/crn/queries/teams.queries.ts b/packages/contentful/src/crn/queries/teams.queries.ts index 7b3098dea2..a5b946ad24 100644 --- a/packages/contentful/src/crn/queries/teams.queries.ts +++ b/packages/contentful/src/crn/queries/teams.queries.ts @@ -34,6 +34,14 @@ export const FETCH_TEAM_BY_ID = gql` } } linkedFrom { + manuscriptsCollection(limit: 20, order: sys_firstPublishedAt_DESC) { + items { + sys { + id + } + title + } + } teamMembershipCollection(limit: 100) { items { role diff --git a/packages/contentful/src/crn/schema/autogenerated-schema.graphql b/packages/contentful/src/crn/schema/autogenerated-schema.graphql index 40e8176ca7..b20b05236f 100644 --- a/packages/contentful/src/crn/schema/autogenerated-schema.graphql +++ b/packages/contentful/src/crn/schema/autogenerated-schema.graphql @@ -2460,6 +2460,7 @@ type Manuscripts implements Entry { contentfulMetadata: ContentfulMetadata! linkedFrom(allowedLocales: [String]): ManuscriptsLinkingCollections sys: Sys! + teamsCollection(limit: Int = 100, locale: String, order: [ManuscriptsTeamsCollectionOrder], preview: Boolean, skip: Int = 0, where: TeamsFilter): ManuscriptsTeamsCollection title(locale: String): String } @@ -2475,6 +2476,8 @@ input ManuscriptsFilter { OR: [ManuscriptsFilter] contentfulMetadata: ContentfulMetadataFilter sys: SysFilter + teams: cfTeamsNestedFilter + teamsCollection_exists: Boolean title: String title_contains: String title_exists: Boolean @@ -2501,6 +2504,30 @@ enum ManuscriptsOrder { title_DESC } +type ManuscriptsTeamsCollection { + items: [Teams]! + limit: Int! + skip: Int! + total: Int! +} + +enum ManuscriptsTeamsCollectionOrder { + applicationNumber_ASC + applicationNumber_DESC + displayName_ASC + displayName_DESC + inactiveSince_ASC + inactiveSince_DESC + sys_firstPublishedAt_ASC + sys_firstPublishedAt_DESC + sys_id_ASC + sys_id_DESC + sys_publishedAt_ASC + sys_publishedAt_DESC + sys_publishedVersion_ASC + sys_publishedVersion_DESC +} + """Videos and PDFs [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/media)""" type Media implements Entry { contentfulMetadata: ContentfulMetadata! @@ -4411,6 +4438,7 @@ type TeamsLinkingCollections { entryCollection(limit: Int = 100, locale: String, preview: Boolean, skip: Int = 0): EntryCollection eventSpeakersCollection(limit: Int = 100, locale: String, order: [TeamsLinkingCollectionsEventSpeakersCollectionOrder], preview: Boolean, skip: Int = 0): EventSpeakersCollection interestGroupsCollection(limit: Int = 100, locale: String, order: [TeamsLinkingCollectionsInterestGroupsCollectionOrder], preview: Boolean, skip: Int = 0): InterestGroupsCollection + manuscriptsCollection(limit: Int = 100, locale: String, order: [TeamsLinkingCollectionsManuscriptsCollectionOrder], preview: Boolean, skip: Int = 0): ManuscriptsCollection researchOutputsCollection(limit: Int = 100, locale: String, order: [TeamsLinkingCollectionsResearchOutputsCollectionOrder], preview: Boolean, skip: Int = 0): ResearchOutputsCollection teamMembershipCollection(limit: Int = 100, locale: String, order: [TeamsLinkingCollectionsTeamMembershipCollectionOrder], preview: Boolean, skip: Int = 0): TeamMembershipCollection tutorialsCollection(limit: Int = 100, locale: String, order: [TeamsLinkingCollectionsTutorialsCollectionOrder], preview: Boolean, skip: Int = 0): TutorialsCollection @@ -4459,6 +4487,19 @@ enum TeamsLinkingCollectionsInterestGroupsCollectionOrder { sys_publishedVersion_DESC } +enum TeamsLinkingCollectionsManuscriptsCollectionOrder { + sys_firstPublishedAt_ASC + sys_firstPublishedAt_DESC + sys_id_ASC + sys_id_DESC + sys_publishedAt_ASC + sys_publishedAt_DESC + sys_publishedVersion_ASC + sys_publishedVersion_DESC + title_ASC + title_DESC +} + enum TeamsLinkingCollectionsResearchOutputsCollectionOrder { accession_ASC accession_DESC diff --git a/packages/contentful/src/mocks/graphql-client.mock.ts b/packages/contentful/src/mocks/graphql-client.mock.ts index 7ad412a14d..2673d44f33 100644 --- a/packages/contentful/src/mocks/graphql-client.mock.ts +++ b/packages/contentful/src/mocks/graphql-client.mock.ts @@ -68,6 +68,7 @@ export const getContentfulGraphqlClientMockServer = ( InterestGroupsCollection: resultDto, InterestGroupLeadersCollection: resultDto, LabsCollection: resultDto, + ManuscriptsCollection: resultDto, NewsCollection: resultDto, PagesCollection: resultDto, ResearchOutputsCollection: resultDto, diff --git a/packages/fixtures/src/manuscripts.ts b/packages/fixtures/src/manuscripts.ts index 86a6db20e8..1cf478db7d 100644 --- a/packages/fixtures/src/manuscripts.ts +++ b/packages/fixtures/src/manuscripts.ts @@ -5,4 +5,5 @@ export const createManuscriptResponse = ( ): ManuscriptResponse => ({ id: `manuscript_${itemIndex}`, title: `Manuscript ${itemIndex + 1}`, + teamId: 'team-1', }); diff --git a/packages/fixtures/src/teams.ts b/packages/fixtures/src/teams.ts index 7f3671195c..d8699d55ea 100644 --- a/packages/fixtures/src/teams.ts +++ b/packages/fixtures/src/teams.ts @@ -27,6 +27,7 @@ const listTeamResponseItem: Omit = { 'Mapping the LRRK2 signalling pathway and its interplay with other Parkinson’s disease components', tags: [], members: [], + manuscripts: [], lastModifiedDate: '2020-09-07T17:36:54Z', labCount: 0, }; diff --git a/packages/model/src/manuscript.ts b/packages/model/src/manuscript.ts index 725799d86a..574328b704 100644 --- a/packages/model/src/manuscript.ts +++ b/packages/model/src/manuscript.ts @@ -3,11 +3,15 @@ import { JSONSchemaType } from 'ajv'; export type ManuscriptDataObject = { id: string; title: string; + teamId: string; }; export type ManuscriptResponse = ManuscriptDataObject; -export type ManuscriptPostRequest = Pick; +export type ManuscriptPostRequest = Pick< + ManuscriptDataObject, + 'title' | 'teamId' +>; export type ManuscriptCreateDataObject = ManuscriptPostRequest; @@ -16,6 +20,7 @@ export const manuscriptPostRequestSchema: JSONSchemaType type: 'object', properties: { title: { type: 'string' }, + teamId: { type: 'string' }, }, required: ['title'], additionalProperties: false, diff --git a/packages/model/src/team.ts b/packages/model/src/team.ts index 02b2a81787..0a856c790e 100644 --- a/packages/model/src/team.ts +++ b/packages/model/src/team.ts @@ -1,5 +1,6 @@ import { FetchOptions, ListResponse } from './common'; import { LabResponse } from './lab'; +import { ManuscriptResponse } from './manuscript'; import { ResearchTagDataObject } from './research-tag'; export const teamRole = [ @@ -45,6 +46,8 @@ export interface TeamMember { inactiveSinceDate?: string; } +export type TeamManuscript = Pick; + export type TeamDataObject = Omit & { id: string; tags: Pick[]; @@ -52,6 +55,7 @@ export type TeamDataObject = Omit & { lastModifiedDate: string; pointOfContact?: TeamMember; tools?: TeamTool[]; + manuscripts: TeamManuscript[]; labCount: number; inactiveSince?: string; }; diff --git a/packages/react-components/src/templates/ManuscriptForm.tsx b/packages/react-components/src/templates/ManuscriptForm.tsx index 0d16b97576..404dd1dc2c 100644 --- a/packages/react-components/src/templates/ManuscriptForm.tsx +++ b/packages/react-components/src/templates/ManuscriptForm.tsx @@ -45,11 +45,13 @@ const buttonsInnerContainerStyles = css({ type ManuscriptFormProps = { onSave: (output: ManuscriptPostRequest) => Promise; onSuccess: () => void; + teamId: string; }; const ManuscriptForm: React.FC = ({ onSave, onSuccess, + teamId, }) => { const history = useHistory(); @@ -67,7 +69,7 @@ const ManuscriptForm: React.FC = ({ } = methods; const onSubmit = async (data: ManuscriptPostRequest) => { - await onSave(data); + await onSave({ ...data, teamId }); onSuccess(); }; @@ -80,16 +82,21 @@ const ManuscriptForm: React.FC = ({ ( + rules={{ + required: 'Please enter a title.', + maxLength: { + value: 256, + message: 'This title cannot exceed 256 characters.', + }, + }} + render={({ + field: { value, onChange }, + fieldState: { error }, + }) => ( - validationState.valueMissing ? 'Please enter a title.' : '' - } - required + customValidationMessage={error?.message} value={value} onChange={onChange} enabled={!isSubmitting} diff --git a/packages/react-components/src/templates/TeamProfileWorkspace.tsx b/packages/react-components/src/templates/TeamProfileWorkspace.tsx index ae1de6cc4a..cb7e80e036 100644 --- a/packages/react-components/src/templates/TeamProfileWorkspace.tsx +++ b/packages/react-components/src/templates/TeamProfileWorkspace.tsx @@ -2,10 +2,19 @@ import { isEnabled } from '@asap-hub/flags'; import { TeamResponse, TeamTool } from '@asap-hub/model'; import { network } from '@asap-hub/routing'; import { css } from '@emotion/react'; +import { colors } from '..'; -import { Caption, Card, Display, Headline2, Link, Paragraph } from '../atoms'; +import { + Caption, + Card, + Display, + Headline2, + Link, + Paragraph, + Subtitle, +} from '../atoms'; import { formatDateAndTime } from '../date'; -import { plusIcon } from '../icons'; +import { plusIcon, plusRectIcon } from '../icons'; import { createMailTo, mailToSupport } from '../mail'; import { ToolCard } from '../organisms'; import { mobileScreen, perRem, rem } from '../pixels'; @@ -39,6 +48,17 @@ const manuscriptButtonStyles = css({ gap: rem(8), }); +const manuscriptContainerStyles = css({ + display: 'flex', + alignItems: 'center', + gap: rem(16), + marginTop: rem(12), + padding: `${rem(24)} ${rem(16)}`, + backgroundColor: colors.pearl.rgb, + border: `1px solid ${colors.steel.rgb}`, + borderRadius: '8px', +}); + const toolContainerStyles = css({ listStyle: 'none', margin: 0, @@ -49,7 +69,10 @@ const toolContainerStyles = css({ }); type TeamProfileWorkspaceProps = Readonly< - Pick + Pick< + TeamResponse, + 'id' | 'pointOfContact' | 'lastModifiedDate' | 'manuscripts' + > > & { readonly tools: ReadonlyArray; readonly onDeleteTool?: (toolIndex: number) => Promise; @@ -59,7 +82,7 @@ const TeamProfileWorkspace: React.FC = ({ id, pointOfContact, lastModifiedDate, - + manuscripts, tools, onDeleteTool, }) => { @@ -89,11 +112,16 @@ const TeamProfileWorkspace: React.FC = ({ - Submit your manuscripts to receive a compliance report and find - out which areas need to be improved before publishing your - article. + This directory contains all manuscripts with their compliance + reports. + {manuscripts.map((manuscript) => ( +
+ {plusRectIcon} + {manuscript.title} +
+ ))} )} diff --git a/packages/react-components/src/templates/__tests__/ManuscriptForm.test.tsx b/packages/react-components/src/templates/__tests__/ManuscriptForm.test.tsx index 86dad8800b..b5ce948e88 100644 --- a/packages/react-components/src/templates/__tests__/ManuscriptForm.test.tsx +++ b/packages/react-components/src/templates/__tests__/ManuscriptForm.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { ComponentProps } from 'react'; import { MemoryRouter, Route, Router, StaticRouter } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; @@ -12,9 +12,12 @@ beforeEach(() => { history = createMemoryHistory(); }); +const teamId = '42'; + const defaultProps: ComponentProps = { onSave: jest.fn(() => Promise.resolve()), onSuccess: jest.fn(), + teamId, }; it('renders the form', async () => { @@ -43,7 +46,7 @@ it('title is sent on form submission', async () => { ); userEvent.click(screen.getByRole('button', { name: /Submit/i })); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith({ title: 'manuscript title' }); + expect(onSave).toHaveBeenCalledWith({ title: 'manuscript title', teamId }); }); }); @@ -55,14 +58,55 @@ it('displays error message when manuscript title is missing', async () => { ); const input = screen.getByRole('textbox', { name: /Title of Manuscript/i }); - fireEvent.focusOut(input); - expect(screen.getByText(/Please enter a title/i)).toBeVisible(); + const submitButton = screen.getByRole('button', { name: /Submit/i }); + + userEvent.click(submitButton); + + await waitFor(() => { + expect(submitButton).toBeEnabled(); + }); + expect( + screen.getAllByText(/Please enter a title/i).length, + ).toBeGreaterThanOrEqual(1); userEvent.type(input, 'title'); - fireEvent.focusOut(input); + + userEvent.click(submitButton); + + await waitFor(() => { + expect(submitButton).toBeEnabled(); + }); expect(screen.queryByText(/Please enter a title/i)).toBeNull(); }); +it('displays error message when manuscript title is bigger than 256 characters', async () => { + render( + + + , + ); + + const input = screen.getByRole('textbox', { + name: /Title of Manuscript/i, + }); + userEvent.type( + input, + "Advancements in Parkinson's Disease Research: Investigating the Role of Genetic Mutations and DNA Sequencing Technologies in Unraveling the Molecular Mechanisms, Identifying Biomarkers, and Developing Targeted Therapies for Improved Diagnosis and Treatment of Parkinson Disease", + ); + + const submitButton = screen.getByRole('button', { name: /Submit/i }); + + userEvent.click(submitButton); + + await waitFor(() => { + expect(submitButton).toBeEnabled(); + }); + + expect( + screen.getAllByText(/This title cannot exceed 256 characters./i).length, + ).toBeGreaterThanOrEqual(1); +}); + it('does not submit when required values are missing', async () => { const onSave = jest.fn(); render( diff --git a/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx index 2f22825285..1a1316faf3 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx @@ -13,6 +13,7 @@ const boilerplateProps: ComponentProps = { projectTitle: 'Unknown', members: [], tags: [], + manuscripts: [], lastModifiedDate: formatISO(new Date()), teamListElementId: '', labCount: 15, diff --git a/packages/react-components/src/templates/__tests__/TeamProfilePage.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfilePage.test.tsx index 9bac77d199..62919e30c1 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfilePage.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfilePage.test.tsx @@ -12,6 +12,7 @@ const boilerplateProps: Omit< lastModifiedDate: new Date(2020, 6, 12, 14, 32).toISOString(), members: [], tags: [], + manuscripts: [], teamListElementId: '', labCount: 15, upcomingEventsCount: 0, diff --git a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx index 6b0f22cec4..269b952db5 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx @@ -25,14 +25,23 @@ it('renders the team workspace page', () => { }); it('renders compliance section when feature flag is enabled', () => { + const teamWithManuscripts: ComponentProps = { + ...team, + manuscripts: [ + { id: '1', title: 'Nice manuscript' }, + { id: '2', title: 'A Good Manuscript' }, + ], + }; enable('DISPLAY_MANUSCRIPTS'); - const { getByRole, queryByRole, rerender } = render( - , + const { container, getByRole, queryByRole, rerender } = render( + , ); expect(getByRole('heading', { name: 'Compliance' })).toBeInTheDocument(); + expect(container).toHaveTextContent('Nice manuscript'); + expect(container).toHaveTextContent('A Good Manuscript'); disable('DISPLAY_MANUSCRIPTS'); - rerender(); + rerender(); expect(queryByRole('heading', { name: 'Compliance' })).toBeNull(); });