From 8e6cc2fb8ddc69aa109a8cf1c297cc8d3af54639 Mon Sep 17 00:00:00 2001 From: Gabriela Ueno Date: Tue, 5 Nov 2024 08:50:41 -0300 Subject: [PATCH] ASAP-531 Edit Manuscript (#4430) * ASAP-531 Edit Manuscript * fix rebase issue --- .../src/network/teams/TeamManuscript.tsx | 63 +- .../src/network/teams/TeamProfile.tsx | 7 + .../teams/__tests__/TeamManuscript.test.tsx | 1 + .../teams/__tests__/TeamProfile.test.tsx | 5 + .../teams/__tests__/Workspace.test.tsx | 50 +- apps/crn-frontend/src/network/teams/state.ts | 13 +- .../src/controllers/manuscript.controller.ts | 49 +- .../contentful/manuscript.data-provider.ts | 182 +++- .../types/manuscript.data-provider.types.ts | 12 +- .../crn-server/src/routes/manuscript.route.ts | 15 +- .../controllers/manuscript.controller.test.ts | 121 ++- .../manuscript.data-provider.test.ts | 455 +++++++++- .../test/fixtures/manuscript.fixtures.ts | 48 +- .../test/fixtures/teams.fixtures.ts | 16 + .../test/routes/manuscript.route.test.ts | 10 +- .../20241101124959-add-updated-by-field.js | 27 + .../src/crn/autogenerated-gql/gql.ts | 6 +- .../src/crn/autogenerated-gql/graphql.ts | 787 ++++++++++++++++++ .../src/crn/queries/manuscript.queries.ts | 94 +++ .../crn/schema/autogenerated-schema.graphql | 4 + packages/fixtures/src/manuscripts.ts | 4 + packages/model/src/external-author.ts | 4 +- packages/model/src/manuscript.ts | 354 ++++---- packages/react-components/src/icons/index.ts | 1 + .../react-components/src/icons/pencil.tsx | 25 + .../src/molecules/LabeledRadioButtonGroup.tsx | 4 +- .../src/organisms/ManuscriptAuthors.tsx | 15 +- .../src/organisms/ManuscriptCard.tsx | 1 + .../src/organisms/ManuscriptVersionCard.tsx | 134 ++- .../__tests__/ManuscriptAuthors.test.tsx | 40 + .../__tests__/ManuscriptVersionCard.test.tsx | 209 +++-- .../src/templates/ManuscriptForm.tsx | 267 +++--- .../__tests__/ManuscriptForm.test.tsx | 220 +++-- .../__tests__/TeamProfileWorkspace.test.tsx | 37 +- packages/routing/src/network.ts | 7 +- 35 files changed, 2792 insertions(+), 495 deletions(-) create mode 100644 packages/contentful/migrations/crn/manuscripts/20241101124959-add-updated-by-field.js create mode 100644 packages/react-components/src/icons/pencil.tsx diff --git a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx index 8c898f1383..c514bf4cd7 100644 --- a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx +++ b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx @@ -1,10 +1,11 @@ import { Frame } from '@asap-hub/frontend-utils'; +import { AuthorResponse, AuthorSelectOption } from '@asap-hub/model'; import { ManuscriptForm, ManuscriptHeader, usePushFromHere, } from '@asap-hub/react-components'; -import { network } from '@asap-hub/routing'; +import { network, useRouteParams } from '@asap-hub/routing'; import { FormProvider, useForm } from 'react-hook-form'; import { useSetRecoilState } from 'recoil'; import { @@ -14,18 +15,31 @@ import { } from '../../shared-state'; import { refreshTeamState, + useManuscriptById, usePostManuscript, + usePutManuscript, useTeamById, useUploadManuscriptFile, } from './state'; import { useEligibilityReason } from './useEligibilityReason'; import { useManuscriptToast } from './useManuscriptToast'; +const useParamManuscriptVersion = (teamId: string): string => { + const route = network({}) + .teams({}) + .team({ teamId }) + .workspace({}).editManuscript; + const { manuscriptId } = useRouteParams(route); + return manuscriptId; +}; + type TeamManuscriptProps = { teamId: string; }; const TeamManuscript: React.FC = ({ teamId }) => { const setRefreshTeamState = useSetRecoilState(refreshTeamState(teamId)); + const manuscriptId = useParamManuscriptVersion(teamId); + const manuscript = useManuscriptById(manuscriptId); const team = useTeamById(teamId); @@ -33,6 +47,7 @@ const TeamManuscript: React.FC = ({ teamId }) => { const { setFormType } = useManuscriptToast(); const form = useForm(); const createManuscript = usePostManuscript(); + const updateManuscript = usePutManuscript(); const handleFileUpload = useUploadManuscriptFile(); const getTeamSuggestions = useTeamSuggestions(); const getLabSuggestions = useLabSuggestions(); @@ -47,27 +62,57 @@ const TeamManuscript: React.FC = ({ teamId }) => { pushFromHere(path); }; - const selectedTeams = [ + const { + teams: manuscriptTeams, + labs: manuscriptLabs, + firstAuthors: manuscriptFirstAuthors, + correspondingAuthor: manuscriptCorrespondingAuthor, + additionalAuthors: manuscriptAdditionalAuthors, + ...manuscriptVersion + } = manuscript?.versions[0] || {}; + const selectedTeams = manuscriptTeams?.map((selectedTeam, index) => ({ + value: selectedTeam.id, + label: selectedTeam.displayName, + isFixed: index === 0, + })) || [ { - label: team?.displayName || '', value: teamId, + label: team?.displayName || '', isFixed: true, }, ]; + const selectedLabs = (manuscriptLabs || []).map((lab) => ({ + value: lab.id, + label: lab.name, + isFixed: false, + })); + + const convertAuthorsToSelectOptions = ( + authors: AuthorResponse[] | undefined, + ) => + (authors || []).map((author) => ({ + author, + label: author.displayName, + value: author.id, + })) as (AuthorResponse & AuthorSelectOption)[]; + return ( getAuthorSuggestions(input).then((authors) => authors.map((author) => ({ @@ -77,9 +122,19 @@ const TeamManuscript: React.FC = ({ teamId }) => { })), ) } + title={manuscript?.title} + firstAuthors={convertAuthorsToSelectOptions(manuscriptFirstAuthors)} + correspondingAuthor={convertAuthorsToSelectOptions( + manuscriptCorrespondingAuthor, + )} + additionalAuthors={convertAuthorsToSelectOptions( + manuscriptAdditionalAuthors, + )} + {...manuscriptVersion} /> ); }; + export default TeamManuscript; diff --git a/apps/crn-frontend/src/network/teams/TeamProfile.tsx b/apps/crn-frontend/src/network/teams/TeamProfile.tsx index b8ce21ec15..bb46ccbe27 100644 --- a/apps/crn-frontend/src/network/teams/TeamProfile.tsx +++ b/apps/crn-frontend/src/network/teams/TeamProfile.tsx @@ -149,6 +149,13 @@ const TeamProfile: FC = ({ currentTime }) => { + + + + + {canCreateComplianceReport && ( ({ createManuscript: jest.fn().mockResolvedValue(manuscriptResponse), + getManuscript: jest.fn().mockResolvedValue(null), uploadManuscriptFile: jest.fn().mockResolvedValue({ filename: 'manuscript.pdf', url: 'https://example.com/manuscript.pdf', 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 389541081d..006527adeb 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx @@ -194,6 +194,7 @@ it('displays manuscript success toast message and user can dismiss toast', async expect(await screen.findByText(/tools/i)).toBeVisible(); userEvent.click(screen.getByText(/Submit Manuscript/i)); + userEvent.click(screen.getByText(/Yes/i)); userEvent.click( @@ -203,6 +204,10 @@ it('displays manuscript success toast message and user can dismiss toast', async ); userEvent.click(screen.getByText(/Continue/i)); + await waitFor(() => + expect(screen.queryByText(/loading/i)).not.toBeInTheDocument(), + ); + const submitButton = screen.getByRole('button', { name: /Submit/i }); await waitFor(() => { diff --git a/apps/crn-frontend/src/network/teams/__tests__/Workspace.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/Workspace.test.tsx index c378e78bd8..f92e882fef 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/Workspace.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/Workspace.test.tsx @@ -372,20 +372,21 @@ describe('manuscript quick check discussion', () => { enable('DISPLAY_MANUSCRIPTS'); mockGetDiscussion.mockResolvedValueOnce(acknowledgedGrantNumberDiscussion); - const { getByText, findByTestId, getByTestId } = renderWithWrapper( - , - ); + const { getByText, findByTestId, getByLabelText, getByTestId } = + renderWithWrapper( + , + ); await act(async () => { userEvent.click(await findByTestId('collapsible-button')); - userEvent.click(getByTestId('version-collapsible-button')); + userEvent.click(getByLabelText('Expand Version')); }); userEvent.click(getByTestId('discussion-collapsible-button')); @@ -402,22 +403,23 @@ describe('manuscript quick check discussion', () => { enable('DISPLAY_MANUSCRIPTS'); mockGetDiscussion.mockResolvedValue(acknowledgedGrantNumberDiscussion); mockUpdateDiscussion.mockResolvedValue(acknowledgedGrantNumberDiscussion); - const { findByTestId, getByRole, getByTestId } = renderWithWrapper( - - - , - ); + const { findByTestId, getByRole, getByTestId, getByLabelText } = + renderWithWrapper( + + + , + ); await act(async () => { userEvent.click(await findByTestId('collapsible-button')); - userEvent.click(getByTestId('version-collapsible-button')); + userEvent.click(getByLabelText('Expand Version')); }); userEvent.click(getByTestId('discussion-collapsible-button')); diff --git a/apps/crn-frontend/src/network/teams/state.ts b/apps/crn-frontend/src/network/teams/state.ts index e6470cc276..a81f1ee9f9 100644 --- a/apps/crn-frontend/src/network/teams/state.ts +++ b/apps/crn-frontend/src/network/teams/state.ts @@ -13,6 +13,7 @@ import { DiscussionResponse, } from '@asap-hub/model'; import { useCurrentUserCRN } from '@asap-hub/react-context'; +import { useCallback } from 'react'; import { atom, atomFamily, @@ -186,6 +187,14 @@ export const manuscriptState = atomFamily< default: fetchManuscriptState, }); +export const useInvalidateManuscriptIndex = () => { + const [refresh, setRefresh] = useRecoilState(refreshManuscriptIndex); + + return useCallback(() => { + setRefresh(refresh + 1); + }, [refresh, setRefresh]); +}; + export const useManuscriptById = (id: string) => useRecoilValue(manuscriptState(id)); @@ -210,10 +219,12 @@ export const usePostManuscript = () => { export const usePutManuscript = () => { const authorization = useRecoilValue(authorizationState); const setManuscriptItem = useSetManuscriptItem(); + const invalidateManuscriptIndex = useInvalidateManuscriptIndex(); + return async (id: string, payload: ManuscriptPutRequest) => { const manuscript = await updateManuscript(id, payload, authorization); setManuscriptItem(manuscript); - + invalidateManuscriptIndex(); return manuscript; }; }; diff --git a/apps/crn-server/src/controllers/manuscript.controller.ts b/apps/crn-server/src/controllers/manuscript.controller.ts index 2701019c76..ba88b720f9 100644 --- a/apps/crn-server/src/controllers/manuscript.controller.ts +++ b/apps/crn-server/src/controllers/manuscript.controller.ts @@ -4,8 +4,8 @@ import { ManuscriptFileResponse, ManuscriptFileType, ManuscriptPostAuthor, + ManuscriptPutRequest, ManuscriptResponse, - ManuscriptUpdateDataObject, } from '@asap-hub/model'; import { @@ -138,7 +138,8 @@ export default class ManuscriptController { async update( id: string, - manuscriptData: ManuscriptUpdateDataObject, + manuscriptData: ManuscriptPutRequest, + userId: string, ): Promise { const currentManuscript = await this.manuscriptDataProvider.fetchById(id); @@ -146,7 +147,49 @@ export default class ManuscriptController { throw new NotFoundError(undefined, `manuscript with id ${id} not found`); } - await this.manuscriptDataProvider.update(id, manuscriptData); + if ('status' in manuscriptData && manuscriptData.status) { + await this.manuscriptDataProvider.update(id, manuscriptData, userId); + return this.fetchById(id); + } + + if ('versions' in manuscriptData && manuscriptData.versions?.[0]) { + const { + firstAuthors, + correspondingAuthor, + additionalAuthors, + ...versionData + } = manuscriptData.versions[0]; + + const firstAuthorsValues = await this.mapAuthorsPostRequestToId( + firstAuthors ?? [], + ); + const correspondingAuthorValues = correspondingAuthor + ? await this.mapAuthorsPostRequestToId([correspondingAuthor] ?? []) + : []; + + const additionalAuthorsValues = await this.mapAuthorsPostRequestToId( + additionalAuthors ?? [], + ); + + const getValidAuthorIds = (authorIds: (string | null)[]) => + authorIds.filter((authorId): authorId is string => authorId !== null); + + await this.manuscriptDataProvider.update( + id, + { + ...manuscriptData, + versions: [ + { + ...versionData, + firstAuthors: getValidAuthorIds(firstAuthorsValues), + correspondingAuthor: getValidAuthorIds(correspondingAuthorValues), + additionalAuthors: getValidAuthorIds(additionalAuthorsValues), + }, + ], + }, + userId, + ); + } return this.fetchById(id); } 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 f2f710e26a..a1ad926ccc 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 @@ -80,21 +80,14 @@ export class ManuscriptContentfulDataProvider return parseGraphQLManuscript(manuscripts); } - - async create(input: ManuscriptCreateDataObject): Promise { + private async createManuscriptAssets( + version: Pick< + ManuscriptCreateDataObject['versions'][number], + 'manuscriptFile' | 'keyResourceTable' | 'additionalFiles' + >, + ) { const environment = await this.getRestClient(); - const { - teamId, - userId, - versions: [version], - ...plainFields - } = input; - - if (!version) { - throw new Error('No versions provided'); - } - const manuscriptFileAsset = await environment.getAsset( version.manuscriptFile.id, ); @@ -111,6 +104,16 @@ export class ManuscriptContentfulDataProvider const additionalFileAsset = await environment.getAsset(additionalFile.id); await additionalFileAsset.publish(); }); + } + + private async createQuickChecks( + version: Pick< + ManuscriptCreateDataObject['versions'][number], + QuickCheckDetails + >, + userId: string, + ) { + const environment = await this.getRestClient(); const quickCheckDetails = { asapAffiliationIncludedDetails: version.asapAffiliationIncludedDetails, @@ -129,6 +132,26 @@ export class ManuscriptContentfulDataProvider userId, ); + return quickCheckDiscussions; + } + + async create(input: ManuscriptCreateDataObject): Promise { + const environment = await this.getRestClient(); + + const { + teamId, + userId, + versions: [version], + ...plainFields + } = input; + + if (!version) { + throw new Error('No versions provided'); + } + + await this.createManuscriptAssets(version); + const quickCheckDiscussions = await this.createQuickChecks(version, userId); + const manuscriptVersionEntry = await environment.createEntry( 'manuscriptVersions', { @@ -152,6 +175,7 @@ export class ManuscriptContentfulDataProvider ) : null, createdBy: getLinkEntity(userId), + updatedBy: getLinkEntity(userId), ...quickCheckDiscussions, }), }, @@ -180,11 +204,55 @@ export class ManuscriptContentfulDataProvider async update( id: string, manuscriptData: ManuscriptUpdateDataObject, + userId: string, ): Promise { const environment = await this.getRestClient(); - const entry = await environment.getEntry(id); + const manuscriptEntry = await environment.getEntry(id); - await patchAndPublish(entry, { status: manuscriptData.status }); + if ('status' in manuscriptData) { + await patchAndPublish(manuscriptEntry, { + status: manuscriptData.status, + }); + } + + if ('versions' in manuscriptData && manuscriptData.versions?.[0]) { + const version = manuscriptData.versions[0]; + + await this.createManuscriptAssets(version); + const quickCheckDiscussions = await this.createQuickChecks( + version, + userId, + ); + + const versionId = manuscriptEntry.fields.versions['en-US'][0].sys.id; + + const versionEntry = await environment.getEntry(versionId); + await patchAndPublish(manuscriptEntry, { + title: manuscriptData.title, + }); + + await patchAndPublish(versionEntry, { + ...version, + teams: getLinkEntities(version.teams), + labs: version?.labs?.length ? getLinkEntities(version.labs) : [], + firstAuthors: getLinkEntities(version.firstAuthors), + correspondingAuthor: getLinkEntities(version.correspondingAuthor), + additionalAuthors: getLinkEntities(version.additionalAuthors), + manuscriptFile: getLinkAsset(version.manuscriptFile.id), + keyResourceTable: version.keyResourceTable + ? getLinkAsset(version.keyResourceTable.id) + : null, + additionalFiles: version.additionalFiles?.length + ? getLinkAssets( + version.additionalFiles.map( + (additionalFile) => additionalFile.id, + ), + ) + : null, + updatedBy: getLinkEntity(userId), + ...quickCheckDiscussions, + }); + } } } @@ -201,6 +269,58 @@ const parseGraphQLManuscript = ( ), }); +type ManuscriptVersionItem = NonNullable< + NonNullable< + NonNullable['versionsCollection'] + >['items'][number] +>; + +type FirstAuthorItem = NonNullable< + NonNullable['items'][number] +>; + +type CorrespondingAuthorItem = NonNullable< + NonNullable< + ManuscriptVersionItem['correspondingAuthorCollection'] + >['items'][number] +>; + +type AdditionalAuthorItem = NonNullable< + NonNullable< + ManuscriptVersionItem['additionalAuthorsCollection'] + >['items'][number] +>; + +const parseGraphqlAuthor = ( + authorItems: + | FirstAuthorItem[] + | CorrespondingAuthorItem[] + | AdditionalAuthorItem[], +) => + authorItems.map((author) => { + if (author.__typename === 'Users') { + return { + id: author.sys.id, + firstName: author.firstName || '', + lastName: author.lastName || '', + email: author.email || '', + displayName: parseUserDisplayName( + author.firstName || '', + author.lastName || '', + undefined, + author.nickname || '', + ), + avatarUrl: author.avatar?.url || undefined, + }; + } + + return { + id: author.sys.id, + displayName: author?.name || '', + email: author.email || '', + }; + }); + export const parseGraphqlManuscriptVersion = ( versions: NonNullable< NonNullable['items'] @@ -286,6 +406,23 @@ export const parseGraphqlManuscriptVersion = ( name: teamItem?.team?.displayName, })), }, + updatedBy: { + id: version?.updatedBy?.sys.id || '', + firstName: version?.updatedBy?.firstName || '', + lastName: version?.updatedBy?.lastName || '', + displayName: parseUserDisplayName( + version?.updatedBy?.firstName || '', + version?.updatedBy?.lastName || '', + undefined, + version?.updatedBy?.nickname || '', + ), + avatarUrl: version?.updatedBy?.avatar?.url || undefined, + alumniSinceDate: version?.updatedBy?.alumniSinceDate || undefined, + teams: version?.updatedBy?.teamsCollection?.items.map((teamItem) => ({ + id: teamItem?.team?.sys.id, + name: teamItem?.team?.displayName, + })), + }, createdDate: version?.sys.firstPublishedAt, publishedAt: version?.sys.publishedAt, teams: version?.teamsCollection?.items.map((teamItem) => ({ @@ -300,6 +437,21 @@ export const parseGraphqlManuscriptVersion = ( complianceReport: parseComplianceReport( version?.linkedFrom?.complianceReportsCollection?.items[0], ), + firstAuthors: parseGraphqlAuthor( + (version?.firstAuthorsCollection?.items || []).filter( + (author): author is FirstAuthorItem => author !== null, + ), + ), + additionalAuthors: parseGraphqlAuthor( + (version?.additionalAuthorsCollection?.items || []).filter( + (author): author is AdditionalAuthorItem => author !== null, + ), + ), + correspondingAuthor: parseGraphqlAuthor( + (version?.correspondingAuthorCollection?.items || []).filter( + (author): author is CorrespondingAuthorItem => author !== null, + ), + ), })) .filter( (version) => diff --git a/apps/crn-server/src/data-providers/types/manuscript.data-provider.types.ts b/apps/crn-server/src/data-providers/types/manuscript.data-provider.types.ts index b1a7023f00..0da9fbd6b9 100644 --- a/apps/crn-server/src/data-providers/types/manuscript.data-provider.types.ts +++ b/apps/crn-server/src/data-providers/types/manuscript.data-provider.types.ts @@ -9,7 +9,11 @@ export type ManuscriptDataProvider = DataProvider< ManuscriptDataObject, ManuscriptDataObject, null, - ManuscriptCreateDataObject, - null, - ManuscriptUpdateDataObject ->; + ManuscriptCreateDataObject +> & { + update( + id: string, + data: ManuscriptUpdateDataObject, + userId: string, + ): Promise; +}; diff --git a/apps/crn-server/src/routes/manuscript.route.ts b/apps/crn-server/src/routes/manuscript.route.ts index 4113c0e653..02d5f36a4e 100644 --- a/apps/crn-server/src/routes/manuscript.route.ts +++ b/apps/crn-server/src/routes/manuscript.route.ts @@ -99,18 +99,25 @@ export const manuscriptRouteFactory = ( '/manuscripts/:manuscriptId', async (req, res: Response) => { const { params, loggedInUser, body } = req; + const payload = validateManuscriptPutRequestParameters(body); if ( !loggedInUser || - !(loggedInUser.role === 'Staff' && loggedInUser.openScienceTeamMember) + ('status' in payload && + payload.status && + !( + loggedInUser.role === 'Staff' && loggedInUser.openScienceTeamMember + )) ) throw Boom.forbidden(); const { manuscriptId } = params; - const payload = validateManuscriptPutRequestParameters(body); - - const result = await manuscriptController.update(manuscriptId, payload); + const result = await manuscriptController.update( + manuscriptId, + payload, + loggedInUser.id, + ); res.json(result); }, diff --git a/apps/crn-server/test/controllers/manuscript.controller.test.ts b/apps/crn-server/test/controllers/manuscript.controller.test.ts index eaab02efb4..0ac73a894e 100644 --- a/apps/crn-server/test/controllers/manuscript.controller.test.ts +++ b/apps/crn-server/test/controllers/manuscript.controller.test.ts @@ -14,7 +14,7 @@ import { getManuscriptCreateDataObject, getManuscriptFileResponse, getManuscriptCreateControllerDataObject, - getManuscriptUpdateDataObject, + getManuscriptUpdateStatusDataObject, } from '../fixtures/manuscript.fixtures'; import { getDataProviderMock } from '../mocks/data-provider.mock'; @@ -299,7 +299,8 @@ describe('Manuscript controller', () => { await expect( manuscriptController.update( manuscriptId, - getManuscriptUpdateDataObject(), + { status: 'Manuscript Resubmitted' }, + 'user-id', ), ).rejects.toThrow(GenericError); }); @@ -314,12 +315,13 @@ describe('Manuscript controller', () => { await expect( manuscriptController.update( manuscriptId, - getManuscriptUpdateDataObject(), + { status: 'Manuscript Resubmitted' }, + 'user-id', ), ).rejects.toThrow(NotFoundError); }); - test('Should update the manuscript and return it', async () => { + test('Should update the manuscript status and return it', async () => { const manuscriptId = 'manuscript-id-1'; manuscriptDataProviderMock.fetchById.mockResolvedValue( getManuscriptResponse(), @@ -328,13 +330,120 @@ describe('Manuscript controller', () => { const result = await manuscriptController.update( manuscriptId, - getManuscriptUpdateDataObject(), + { status: 'Manuscript Resubmitted' }, + 'user-id', ); expect(result).toEqual(getManuscriptResponse()); expect(manuscriptDataProviderMock.update).toHaveBeenCalledWith( manuscriptId, - getManuscriptUpdateDataObject(), + getManuscriptUpdateStatusDataObject(), + 'user-id', + ); + }); + + test('Should update the manuscript version and return it', async () => { + const manuscriptId = 'manuscript-id-1'; + manuscriptDataProviderMock.fetchById.mockResolvedValue( + getManuscriptResponse(), + ); + manuscriptDataProviderMock.update.mockResolvedValueOnce(); + + const result = await manuscriptController.update( + manuscriptId, + { + versions: [ + { + lifecycle: 'Preprint', + type: 'Original Research', + teams: ['team-1'], + manuscriptFile: getManuscriptFileResponse(), + description: 'edited description', + firstAuthors: [{ userId: 'author-1' }], + correspondingAuthor: { userId: 'author-2' }, + additionalAuthors: [ + { + externalAuthorId: 'external-1', + externalAuthorName: 'External One', + externalAuthorEmail: 'external@one.com', + }, + ], + }, + ], + }, + 'user-id', + ); + + expect(result).toEqual(getManuscriptResponse()); + expect(manuscriptDataProviderMock.update).toHaveBeenCalledWith( + manuscriptId, + { + versions: [ + { + lifecycle: 'Preprint', + type: 'Original Research', + teams: ['team-1'], + manuscriptFile: { + filename: 'manuscript.pdf', + id: 'file-id', + url: 'https://example.com/manuscript.pdf', + }, + description: 'edited description', + firstAuthors: ['author-1'], + correspondingAuthor: ['author-2'], + additionalAuthors: ['external-1'], + }, + ], + }, + 'user-id', + ); + + expect(externalAuthorDataProviderMock.update).toHaveBeenCalledWith( + 'external-1', + { + email: 'external@one.com', + }, + ); + }); + + test('Should update the manuscript version when corresponding author and additional authors are empty', async () => { + const manuscriptId = 'manuscript-id-1'; + manuscriptDataProviderMock.fetchById.mockResolvedValue( + getManuscriptResponse(), + ); + manuscriptDataProviderMock.update.mockResolvedValueOnce(); + + const result = await manuscriptController.update( + manuscriptId, + { + versions: [ + { + lifecycle: 'Preprint', + type: 'Original Research', + teams: ['team-1'], + manuscriptFile: getManuscriptFileResponse(), + description: 'edited description', + firstAuthors: [{ userId: 'author-1' }], + correspondingAuthor: undefined, + additionalAuthors: [], + }, + ], + }, + 'user-id', + ); + + expect(result).toEqual(getManuscriptResponse()); + expect(manuscriptDataProviderMock.update).toHaveBeenCalledWith( + manuscriptId, + { + versions: [ + expect.objectContaining({ + correspondingAuthor: [], + additionalAuthors: [], + }), + ], + }, + 'user-id', ); }); }); 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 d97f31dd39..d746d72d09 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 @@ -20,7 +20,8 @@ import { getContentfulGraphqlManuscriptVersions, getManuscriptCreateDataObject, getManuscriptDataObject, - getManuscriptUpdateDataObject, + getManuscriptFileResponse, + getManuscriptUpdateStatusDataObject, } from '../../fixtures/manuscript.fixtures'; import { getContentfulGraphql, @@ -56,6 +57,15 @@ describe('Manuscripts Contentful Data Provider', () => { getContentfulGraphqlManuscriptVersions().items[0]?.teamsCollection, ManuscriptVersionsLabsCollection: () => getContentfulGraphqlManuscriptVersions().items[0]?.labsCollection, + ManuscriptVersionsFirstAuthorsCollection: () => + getContentfulGraphqlManuscriptVersions().items[0] + ?.firstAuthorsCollection, + ManuscriptVersionsCorrespondingAuthorCollection: () => + getContentfulGraphqlManuscriptVersions().items[0] + ?.correspondingAuthorCollection, + ManuscriptVersionsAdditionalAuthorsCollection: () => + getContentfulGraphqlManuscriptVersions().items[0] + ?.additionalAuthorsCollection, }); const manuscriptDataProviderMockGraphql = @@ -173,6 +183,117 @@ describe('Manuscripts Contentful Data Provider', () => { }, ); + test('returns authors', async () => { + const manuscript = getContentfulGraphqlManuscript(); + manuscript.versionsCollection!.items[0]!.firstAuthorsCollection!.items = [ + { + __typename: 'Users', + sys: { + id: 'user-id-1', + }, + avatar: null, + firstName: 'Fiona', + lastName: 'First', + nickname: null, + email: 'fiona.first@email.com', + }, + { + __typename: 'ExternalAuthors', + sys: { + id: 'external-id-1', + }, + name: 'First External', + email: 'first.external@email.com', + }, + ]; + + manuscript.versionsCollection!.items[0]!.correspondingAuthorCollection!.items = + [ + { + __typename: 'Users', + sys: { + id: 'corresponding-id-1', + }, + avatar: null, + firstName: 'Connor', + lastName: 'Corresponding', + nickname: null, + email: 'connor.corresponding@email.com', + }, + ]; + + manuscript.versionsCollection!.items[0]!.additionalAuthorsCollection!.items = + [ + { + __typename: 'Users', + sys: { + id: 'additional-id-1', + }, + avatar: null, + firstName: 'Adele', + lastName: 'Additional', + nickname: null, + email: 'adele.additional@email.com', + }, + { + __typename: 'ExternalAuthors', + sys: { + id: 'external-id-1', + }, + name: 'Second External', + email: 'second.external@email.com', + }, + ]; + + contentfulGraphqlClientMock.request.mockResolvedValue({ + manuscripts: manuscript, + }); + + const result = await manuscriptDataProvider.fetchById('1'); + expect(result!.versions[0]!.firstAuthors).toEqual([ + { + avatarUrl: undefined, + displayName: 'Fiona First', + email: 'fiona.first@email.com', + firstName: 'Fiona', + id: 'user-id-1', + lastName: 'First', + }, + { + displayName: 'First External', + email: 'first.external@email.com', + id: 'external-id-1', + }, + ]); + + expect(result!.versions[0]!.correspondingAuthor).toEqual([ + { + avatarUrl: undefined, + displayName: 'Connor Corresponding', + email: 'connor.corresponding@email.com', + firstName: 'Connor', + id: 'corresponding-id-1', + lastName: 'Corresponding', + }, + ]); + + expect(result!.versions[0]!.additionalAuthors).toEqual([ + { + avatarUrl: undefined, + displayName: 'Adele Additional', + email: 'adele.additional@email.com', + firstName: 'Adele', + id: 'additional-id-1', + lastName: 'Additional', + }, + { + displayName: 'Second External', + email: 'second.external@email.com', + id: 'external-id-1', + }, + ]); + }); + test('should default null values to empty strings and arrays', async () => { const manuscript = getContentfulGraphqlManuscript(); manuscript.title = null; @@ -307,6 +428,15 @@ describe('Manuscripts Contentful Data Provider', () => { }, }, }, + updatedBy: { + 'en-US': { + sys: { + id: 'user-id-0', + linkType: 'Entry', + type: 'Link', + }, + }, + }, asapAffiliationIncludedDetails: { 'en-US': null, }, @@ -662,7 +792,7 @@ describe('Manuscripts Contentful Data Provider', () => { const patch: jest.MockedFunction<() => Promise> = jest.fn(); const publish: jest.MockedFunction<() => Promise> = jest.fn(); - test('can update the manuscript', async () => { + test('can update the manuscript status', async () => { const entry = { sys: { publishedVersion: 1, @@ -678,7 +808,8 @@ describe('Manuscripts Contentful Data Provider', () => { const manuscriptId = 'manuscript-id-1'; await manuscriptDataProvider.update( manuscriptId, - getManuscriptUpdateDataObject(), + getManuscriptUpdateStatusDataObject(), + 'user-id-1', ); expect(environmentMock.getEntry).toHaveBeenCalledWith(manuscriptId); @@ -690,5 +821,323 @@ describe('Manuscripts Contentful Data Provider', () => { }, ]); }); + + test('can update the manuscript version content and title', async () => { + const manuscriptId = 'manuscript-id-1'; + const versionId = 'version-id-1'; + const manuscriptEntry = { + sys: { + publishedVersion: 1, + }, + fields: { + versions: { + 'en-US': [{ sys: { id: versionId } }], + }, + }, + patch, + publish, + } as unknown as Entry; + + const entry = { + sys: { + publishedVersion: 1, + }, + fields: {}, + patch, + publish, + } as unknown as Entry; + + const assetMock = { + sys: { id: manuscriptId }, + publish: jest.fn(), + } as unknown as Asset; + + when(environmentMock.getEntry) + .calledWith(manuscriptId) + .mockResolvedValue(manuscriptEntry); + + when(environmentMock.getEntry) + .calledWith(versionId) + .mockResolvedValue(entry); + + environmentMock.getAsset.mockResolvedValue(assetMock); + + patch.mockResolvedValue(entry); + publish.mockResolvedValue(entry); + + await manuscriptDataProvider.update( + manuscriptId, + { + title: 'New Title', + versions: [ + { + lifecycle: 'Preprint', + type: 'Original Research', + teams: ['team-1'], + manuscriptFile: getManuscriptFileResponse(), + description: 'edited description', + firstAuthors: ['author-1'], + correspondingAuthor: ['author-2'], + additionalAuthors: ['external-1'], + keyResourceTable: { + filename: 'manuscript.csv', + url: 'https://example.com/manuscript.csv', + id: 'file-table-id', + }, + additionalFiles: [ + { + filename: 'manuscript.csv', + url: 'https://example.com/manuscript.csv', + id: 'file-additional-id', + }, + ], + }, + ], + }, + 'user-id-1', + ); + + expect(environmentMock.getEntry).toHaveBeenCalledWith(manuscriptId); + expect(patch).toHaveBeenCalledTimes(2); + expect(patch).toHaveBeenNthCalledWith(1, [ + { + op: 'add', + path: '/fields/title', + value: { 'en-US': 'New Title' }, + }, + ]); + + expect(patch).toHaveBeenNthCalledWith( + 2, + expect.arrayContaining([ + { + op: 'add', + path: '/fields/lifecycle', + value: { + 'en-US': 'Preprint', + }, + }, + { + op: 'add', + path: '/fields/type', + value: { + 'en-US': 'Original Research', + }, + }, + { + op: 'add', + path: '/fields/teams', + value: { + 'en-US': [ + { + sys: { + id: 'team-1', + linkType: 'Entry', + type: 'Link', + }, + }, + ], + }, + }, + { + op: 'add', + path: '/fields/manuscriptFile', + value: { + 'en-US': { + sys: { + id: 'file-id', + linkType: 'Asset', + type: 'Link', + }, + }, + }, + }, + { + op: 'add', + path: '/fields/keyResourceTable', + value: { + 'en-US': { + sys: { + id: 'file-table-id', + linkType: 'Asset', + type: 'Link', + }, + }, + }, + }, + { + op: 'add', + path: '/fields/additionalFiles', + value: { + 'en-US': [ + { + sys: { + id: 'file-additional-id', + linkType: 'Asset', + type: 'Link', + }, + }, + ], + }, + }, + { + op: 'add', + path: '/fields/description', + value: { + 'en-US': 'edited description', + }, + }, + { + op: 'add', + path: '/fields/firstAuthors', + value: { + 'en-US': [ + { + sys: { + id: 'author-1', + linkType: 'Entry', + type: 'Link', + }, + }, + ], + }, + }, + { + op: 'add', + path: '/fields/correspondingAuthor', + value: { + 'en-US': [ + { + sys: { + id: 'author-2', + linkType: 'Entry', + type: 'Link', + }, + }, + ], + }, + }, + { + op: 'add', + path: '/fields/additionalAuthors', + value: { + 'en-US': [ + { + sys: { + id: 'external-1', + linkType: 'Entry', + type: 'Link', + }, + }, + ], + }, + }, + + { + op: 'add', + path: '/fields/updatedBy', + value: { + 'en-US': { + sys: { + id: 'user-id-1', + linkType: 'Entry', + type: 'Link', + }, + }, + }, + }, + ]), + ); + }); + + test('can update the manuscript when keyResourceTable and additionalFiles are not passed', async () => { + const manuscriptId = 'manuscript-id-1'; + const versionId = 'version-id-1'; + const manuscriptEntry = { + sys: { + publishedVersion: 1, + }, + fields: { + versions: { + 'en-US': [{ sys: { id: versionId } }], + }, + }, + patch, + publish, + } as unknown as Entry; + + const entry = { + sys: { + publishedVersion: 1, + }, + fields: {}, + patch, + publish, + } as unknown as Entry; + + const assetMock = { + sys: { id: manuscriptId }, + publish: jest.fn(), + } as unknown as Asset; + + when(environmentMock.getEntry) + .calledWith(manuscriptId) + .mockResolvedValue(manuscriptEntry); + + when(environmentMock.getEntry) + .calledWith(versionId) + .mockResolvedValue(entry); + + environmentMock.getAsset.mockResolvedValue(assetMock); + + patch.mockResolvedValue(entry); + publish.mockResolvedValue(entry); + + await manuscriptDataProvider.update( + manuscriptId, + { + title: 'New Title', + versions: [ + { + lifecycle: 'Preprint', + type: 'Original Research', + teams: ['team-1'], + manuscriptFile: getManuscriptFileResponse(), + description: 'edited description', + firstAuthors: ['author-1'], + correspondingAuthor: ['author-2'], + additionalAuthors: ['external-1'], + }, + ], + }, + 'user-id-1', + ); + + expect(environmentMock.getEntry).toHaveBeenCalledWith(manuscriptId); + expect(patch).toHaveBeenCalledTimes(2); + expect(patch).toHaveBeenNthCalledWith(1, [ + { + op: 'add', + path: '/fields/title', + value: { 'en-US': 'New Title' }, + }, + ]); + + expect(patch).toHaveBeenNthCalledWith( + 2, + expect.arrayContaining([ + { + op: 'add', + path: '/fields/keyResourceTable', + value: { 'en-US': null }, + }, + { + op: 'add', + path: '/fields/additionalFiles', + value: { 'en-US': null }, + }, + ]), + ); + }); }); }); diff --git a/apps/crn-server/test/fixtures/manuscript.fixtures.ts b/apps/crn-server/test/fixtures/manuscript.fixtures.ts index f16e546307..a185b66001 100644 --- a/apps/crn-server/test/fixtures/manuscript.fixtures.ts +++ b/apps/crn-server/test/fixtures/manuscript.fixtures.ts @@ -25,6 +25,7 @@ export const getManuscriptDataObject = ( type: 'Original Research', description: 'A good description', createdBy: manuscriptAuthor, + updatedBy: manuscriptAuthor, createdDate: '2020-09-23T20:45:22.000Z', publishedAt: '2020-09-23T20:45:22.000Z', manuscriptFile: { @@ -41,6 +42,9 @@ export const getManuscriptDataObject = ( { id: 'team-1', displayName: 'Test 1', inactiveSince: undefined }, ], labs: [{ id: 'lab-1', name: 'Lab 1' }], + firstAuthors: [], + correspondingAuthor: [], + additionalAuthors: [], }, ], ...data, @@ -115,6 +119,15 @@ export const getContentfulGraphqlManuscriptVersions: () => NonNullable< }, ], }, + firstAuthorsCollection: { + items: [], + }, + correspondingAuthorCollection: { + items: [], + }, + additionalAuthorsCollection: { + items: [], + }, createdBy: { sys: { id: manuscriptAuthor.id, @@ -137,6 +150,28 @@ export const getContentfulGraphqlManuscriptVersions: () => NonNullable< ], }, }, + updatedBy: { + sys: { + id: manuscriptAuthor.id, + }, + firstName: manuscriptAuthor.firstName, + lastName: manuscriptAuthor.lastName, + nickname: 'Tim', + alumniSinceDate: manuscriptAuthor.alumniSinceDate, + avatar: { url: manuscriptAuthor.avatarUrl }, + teamsCollection: { + items: [ + { + team: { + sys: { + id: manuscriptAuthor.teams[0]!.id, + }, + displayName: manuscriptAuthor.teams[0]!.name, + }, + }, + ], + }, + }, }, ], }); @@ -145,11 +180,12 @@ export const getManuscriptPostBody = (): ManuscriptPostRequest => { const { title, teamId, versions } = getManuscriptDataObject(); const { - createdBy: _, - createdDate: __, - id: ___, - publishedAt: ____, - teams: _____, + createdBy: __, + updatedBy: ___, + createdDate: ____, + id: _____, + publishedAt: ______, + teams: _______, ...version } = versions[0]!; return { @@ -223,7 +259,7 @@ export const getManuscriptCreateDataObject = (): ManuscriptCreateDataObject => { }; }; -export const getManuscriptUpdateDataObject = ( +export const getManuscriptUpdateStatusDataObject = ( overrides?: Partial, ): ManuscriptUpdateDataObject => { return { diff --git a/apps/crn-server/test/fixtures/teams.fixtures.ts b/apps/crn-server/test/fixtures/teams.fixtures.ts index dcea8e733d..42fa1dfc16 100644 --- a/apps/crn-server/test/fixtures/teams.fixtures.ts +++ b/apps/crn-server/test/fixtures/teams.fixtures.ts @@ -35,6 +35,14 @@ export const getContentfulGraphql = (teamById = false) => ({ getContentfulGraphqlManuscriptVersions().items[0]?.teamsCollection, ManuscriptVersionsLabsCollection: () => getContentfulGraphqlManuscriptVersions().items[0]?.labsCollection, + ManuscriptVersionsFirstAuthorsCollection: () => + getContentfulGraphqlManuscriptVersions().items[0]?.firstAuthorsCollection, + ManuscriptVersionsCorrespondingAuthorCollection: () => + getContentfulGraphqlManuscriptVersions().items[0] + ?.correspondingAuthorCollection, + ManuscriptVersionsAdditionalAuthorsCollection: () => + getContentfulGraphqlManuscriptVersions().items[0] + ?.additionalAuthorsCollection, }); export const getUsersTeamsCollection = () => ({ @@ -223,6 +231,7 @@ export const getTeamDataObject = (): TeamDataObject => ({ type: 'Original Research', description: 'A good description', createdBy: manuscriptAuthor, + updatedBy: manuscriptAuthor, createdDate: '2020-09-23T20:45:22.000Z', publishedAt: '2020-09-23T20:45:22.000Z', manuscriptFile: { @@ -242,6 +251,9 @@ export const getTeamDataObject = (): TeamDataObject => ({ }, ], labs: [{ id: 'lab-1', name: 'Lab 1' }], + firstAuthors: [], + correspondingAuthor: [], + additionalAuthors: [], }, ], }, @@ -256,6 +268,7 @@ export const getTeamDataObject = (): TeamDataObject => ({ type: 'Original Research', description: 'A good description', createdBy: manuscriptAuthor, + updatedBy: manuscriptAuthor, createdDate: '2020-09-23T20:45:22.000Z', publishedAt: '2020-09-23T20:45:22.000Z', manuscriptFile: { @@ -275,6 +288,9 @@ export const getTeamDataObject = (): TeamDataObject => ({ }, ], labs: [{ id: 'lab-1', name: 'Lab 1' }], + firstAuthors: [], + correspondingAuthor: [], + additionalAuthors: [], }, ], }, diff --git a/apps/crn-server/test/routes/manuscript.route.test.ts b/apps/crn-server/test/routes/manuscript.route.test.ts index debf3fa8e5..13f5971397 100644 --- a/apps/crn-server/test/routes/manuscript.route.test.ts +++ b/apps/crn-server/test/routes/manuscript.route.test.ts @@ -10,7 +10,7 @@ import { getManuscriptFileResponse, getManuscriptPostBody, getManuscriptResponse, - getManuscriptUpdateDataObject, + getManuscriptUpdateStatusDataObject, } from '../fixtures/manuscript.fixtures'; import { loggerMock } from '../mocks/logger.mock'; import { manuscriptControllerMock } from '../mocks/manuscript.controller.mock'; @@ -331,7 +331,7 @@ describe('/manuscripts/ route', () => { describe('PUT /manuscripts/{id}', () => { const manuscriptId = 'manuscript-id-1'; const manuscriptResponse = getManuscriptResponse(); - const manuscriptPutRequest = getManuscriptUpdateDataObject(); + const manuscriptPutRequest = getManuscriptUpdateStatusDataObject(); test('Should return 403 when not allowed to update a manuscript because user is not onboarded', async () => { userMockFactory.mockReturnValueOnce({ @@ -392,6 +392,7 @@ describe('/manuscripts/ route', () => { expect(manuscriptControllerMock.update).toHaveBeenCalledWith( manuscriptId, manuscriptPutRequest, + 'user-id-0', ); expect(response.body).toEqual(manuscriptResponse); }); @@ -406,7 +407,10 @@ describe('/manuscripts/ route', () => { const response = await supertest(app) .put(`/manuscripts/${manuscriptId}`) - .send({ ...manuscriptPutRequest, title: 'New title' }) + .send({ + ...manuscriptPutRequest, + eligibilityReasons: ['New reason'], + }) .set('Accept', 'application/json'); expect(response.status).toEqual(400); diff --git a/packages/contentful/migrations/crn/manuscripts/20241101124959-add-updated-by-field.js b/packages/contentful/migrations/crn/manuscripts/20241101124959-add-updated-by-field.js new file mode 100644 index 0000000000..b1aa6f9b9e --- /dev/null +++ b/packages/contentful/migrations/crn/manuscripts/20241101124959-add-updated-by-field.js @@ -0,0 +1,27 @@ +module.exports.description = 'Add updated-by field'; + +module.exports.up = (migration) => { + const manuscriptVersions = migration.editContentType('manuscriptVersions'); + + manuscriptVersions + .createField('updatedBy') + .name('Updated By') + .type('Link') + .localized(false) + .required(false) + .disabled(false) + .omitted(false) + .validations([ + { + linkContentType: ['users'], + }, + ]) + .linkType('Entry'); + + manuscriptVersions.moveField('updatedBy').afterField('createdBy'); +}; + +module.exports.down = (migration) => { + const manuscriptVersions = migration.editContentType('manuscriptVersions'); + manuscriptVersions.deleteField('updatedBy'); +}; diff --git a/packages/contentful/src/crn/autogenerated-gql/gql.ts b/packages/contentful/src/crn/autogenerated-gql/gql.ts index a630446468..643d51da58 100644 --- a/packages/contentful/src/crn/autogenerated-gql/gql.ts +++ b/packages/contentful/src/crn/autogenerated-gql/gql.ts @@ -77,7 +77,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 status\n count\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n description\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails {\n ...DiscussionsContent\n }\n asapAffiliationIncluded\n asapAffiliationIncludedDetails {\n ...DiscussionsContent\n }\n manuscriptLicense\n manuscriptLicenseDetails {\n ...DiscussionsContent\n }\n datasetsDeposited\n datasetsDepositedDetails {\n ...DiscussionsContent\n }\n codeDeposited\n codeDepositedDetails {\n ...DiscussionsContent\n }\n protocolsDeposited\n protocolsDepositedDetails {\n ...DiscussionsContent\n }\n labMaterialsRegistered\n labMaterialsRegisteredDetails {\n ...DiscussionsContent\n }\n availabilityStatement\n availabilityStatementDetails {\n ...DiscussionsContent\n }\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\n }\n }\n }\n }\n }\n }\n \n': + '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n status\n count\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n description\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails {\n ...DiscussionsContent\n }\n asapAffiliationIncluded\n asapAffiliationIncludedDetails {\n ...DiscussionsContent\n }\n manuscriptLicense\n manuscriptLicenseDetails {\n ...DiscussionsContent\n }\n datasetsDeposited\n datasetsDepositedDetails {\n ...DiscussionsContent\n }\n codeDeposited\n codeDepositedDetails {\n ...DiscussionsContent\n }\n protocolsDeposited\n protocolsDepositedDetails {\n ...DiscussionsContent\n }\n labMaterialsRegistered\n labMaterialsRegisteredDetails {\n ...DiscussionsContent\n }\n availabilityStatement\n availabilityStatementDetails {\n ...DiscussionsContent\n }\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n updatedBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n firstAuthorsCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n additionalAuthorsCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n correspondingAuthorCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\n }\n }\n }\n }\n }\n }\n \n': types.ManuscriptsContentFragmentDoc, '\n query FetchManuscriptById($id: String!, $fetchReplies: Boolean = false) {\n manuscripts(id: $id) {\n ...ManuscriptsContent\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n }\n }\n }\n }\n \n': types.FetchManuscriptByIdDocument, @@ -351,8 +351,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 status\n count\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n description\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails {\n ...DiscussionsContent\n }\n asapAffiliationIncluded\n asapAffiliationIncludedDetails {\n ...DiscussionsContent\n }\n manuscriptLicense\n manuscriptLicenseDetails {\n ...DiscussionsContent\n }\n datasetsDeposited\n datasetsDepositedDetails {\n ...DiscussionsContent\n }\n codeDeposited\n codeDepositedDetails {\n ...DiscussionsContent\n }\n protocolsDeposited\n protocolsDepositedDetails {\n ...DiscussionsContent\n }\n labMaterialsRegistered\n labMaterialsRegisteredDetails {\n ...DiscussionsContent\n }\n availabilityStatement\n availabilityStatementDetails {\n ...DiscussionsContent\n }\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\n }\n }\n }\n }\n }\n }\n \n', -): (typeof documents)['\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n status\n count\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n description\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails {\n ...DiscussionsContent\n }\n asapAffiliationIncluded\n asapAffiliationIncludedDetails {\n ...DiscussionsContent\n }\n manuscriptLicense\n manuscriptLicenseDetails {\n ...DiscussionsContent\n }\n datasetsDeposited\n datasetsDepositedDetails {\n ...DiscussionsContent\n }\n codeDeposited\n codeDepositedDetails {\n ...DiscussionsContent\n }\n protocolsDeposited\n protocolsDepositedDetails {\n ...DiscussionsContent\n }\n labMaterialsRegistered\n labMaterialsRegisteredDetails {\n ...DiscussionsContent\n }\n availabilityStatement\n availabilityStatementDetails {\n ...DiscussionsContent\n }\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\n }\n }\n }\n }\n }\n }\n \n']; + source: '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n status\n count\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n description\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails {\n ...DiscussionsContent\n }\n asapAffiliationIncluded\n asapAffiliationIncludedDetails {\n ...DiscussionsContent\n }\n manuscriptLicense\n manuscriptLicenseDetails {\n ...DiscussionsContent\n }\n datasetsDeposited\n datasetsDepositedDetails {\n ...DiscussionsContent\n }\n codeDeposited\n codeDepositedDetails {\n ...DiscussionsContent\n }\n protocolsDeposited\n protocolsDepositedDetails {\n ...DiscussionsContent\n }\n labMaterialsRegistered\n labMaterialsRegisteredDetails {\n ...DiscussionsContent\n }\n availabilityStatement\n availabilityStatementDetails {\n ...DiscussionsContent\n }\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n updatedBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n firstAuthorsCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n additionalAuthorsCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n correspondingAuthorCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\n }\n }\n }\n }\n }\n }\n \n', +): (typeof documents)['\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n status\n count\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n description\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails {\n ...DiscussionsContent\n }\n asapAffiliationIncluded\n asapAffiliationIncludedDetails {\n ...DiscussionsContent\n }\n manuscriptLicense\n manuscriptLicenseDetails {\n ...DiscussionsContent\n }\n datasetsDeposited\n datasetsDepositedDetails {\n ...DiscussionsContent\n }\n codeDeposited\n codeDepositedDetails {\n ...DiscussionsContent\n }\n protocolsDeposited\n protocolsDepositedDetails {\n ...DiscussionsContent\n }\n labMaterialsRegistered\n labMaterialsRegisteredDetails {\n ...DiscussionsContent\n }\n availabilityStatement\n availabilityStatementDetails {\n ...DiscussionsContent\n }\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n updatedBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n firstAuthorsCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n additionalAuthorsCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n correspondingAuthorCollection(limit: 20) {\n items {\n __typename\n ... on ExternalAuthors {\n sys {\n id\n }\n name\n email\n }\n ... on Users {\n sys {\n id\n }\n avatar {\n url\n }\n firstName\n lastName\n nickname\n email\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\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 18ad6287ec..aec5a44ba2 100644 --- a/packages/contentful/src/crn/autogenerated-gql/graphql.ts +++ b/packages/contentful/src/crn/autogenerated-gql/graphql.ts @@ -3873,6 +3873,7 @@ export type ManuscriptVersions = Entry & sys: Sys; teamsCollection?: Maybe; type?: Maybe; + updatedBy?: Maybe; }; /** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/manuscriptVersions) */ @@ -4100,6 +4101,13 @@ export type ManuscriptVersionsTypeArgs = { locale?: InputMaybe; }; +/** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/manuscriptVersions) */ +export type ManuscriptVersionsUpdatedByArgs = { + locale?: InputMaybe; + preview?: InputMaybe; + where?: InputMaybe; +}; + export type ManuscriptVersionsAdditionalAuthorsCollection = { items: Array>; limit: Scalars['Int']; @@ -4349,6 +4357,8 @@ export type ManuscriptVersionsFilter = { type_not?: InputMaybe; type_not_contains?: InputMaybe; type_not_in?: InputMaybe>>; + updatedBy?: InputMaybe; + updatedBy_exists?: InputMaybe; }; export type ManuscriptVersionsFirstAuthorsCollection = { @@ -11720,6 +11730,7 @@ export type CfManuscriptVersionsNestedFilter = { type_not?: InputMaybe; type_not_contains?: InputMaybe; type_not_in?: InputMaybe>>; + updatedBy_exists?: InputMaybe; }; export type CfMessagesNestedFilter = { @@ -17978,6 +17989,75 @@ export type ManuscriptsContentFragment = Pick< }>; } >; + updatedBy?: Maybe< + Pick< + Users, + 'firstName' | 'nickname' | 'lastName' | 'alumniSinceDate' + > & { + sys: Pick; + avatar?: Maybe>; + teamsCollection?: Maybe<{ + items: Array< + Maybe<{ + team?: Maybe< + Pick & { sys: Pick } + >; + }> + >; + }>; + } + >; + firstAuthorsCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + 'firstName' | 'lastName' | 'nickname' | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; + additionalAuthorsCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + 'firstName' | 'lastName' | 'nickname' | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; + correspondingAuthorCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + 'firstName' | 'lastName' | 'nickname' | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; linkedFrom?: Maybe<{ complianceReportsCollection?: Maybe<{ items: Array< @@ -18576,6 +18656,75 @@ export type FetchManuscriptByIdQuery = { }>; } >; + updatedBy?: Maybe< + Pick< + Users, + 'firstName' | 'nickname' | 'lastName' | 'alumniSinceDate' + > & { + sys: Pick; + avatar?: Maybe>; + teamsCollection?: Maybe<{ + items: Array< + Maybe<{ + team?: Maybe< + Pick & { sys: Pick } + >; + }> + >; + }>; + } + >; + firstAuthorsCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + 'firstName' | 'lastName' | 'nickname' | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; + additionalAuthorsCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + 'firstName' | 'lastName' | 'nickname' | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; + correspondingAuthorCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + 'firstName' | 'lastName' | 'nickname' | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; linkedFrom?: Maybe<{ complianceReportsCollection?: Maybe<{ items: Array< @@ -20795,6 +20944,89 @@ export type FetchTeamByIdQuery = { }>; } >; + updatedBy?: Maybe< + Pick< + Users, + | 'firstName' + | 'nickname' + | 'lastName' + | 'alumniSinceDate' + > & { + sys: Pick; + avatar?: Maybe>; + teamsCollection?: Maybe<{ + items: Array< + Maybe<{ + team?: Maybe< + Pick & { + sys: Pick; + } + >; + }> + >; + }>; + } + >; + firstAuthorsCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + | 'firstName' + | 'lastName' + | 'nickname' + | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; + additionalAuthorsCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + | 'firstName' + | 'lastName' + | 'nickname' + | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; + correspondingAuthorCollection?: Maybe<{ + items: Array< + Maybe< + | ({ __typename: 'ExternalAuthors' } & Pick< + ExternalAuthors, + 'name' | 'email' + > & { sys: Pick }) + | ({ __typename: 'Users' } & Pick< + Users, + | 'firstName' + | 'lastName' + | 'nickname' + | 'email' + > & { + sys: Pick; + avatar?: Maybe>; + }) + > + >; + }>; linkedFrom?: Maybe<{ complianceReportsCollection?: Maybe<{ items: Array< @@ -25352,6 +25584,561 @@ export const ManuscriptsContentFragmentDoc = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'updatedBy' }, + 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: 'firstName' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'nickname' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'lastName' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'alumniSinceDate' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'avatar' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'url' }, + }, + ], + }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'teamsCollection' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'limit' }, + value: { kind: 'IntValue', value: '3' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'items' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'team' }, + 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: 'displayName', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'firstAuthorsCollection' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'limit' }, + value: { kind: 'IntValue', value: '20' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'items' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { + kind: 'Name', + value: 'ExternalAuthors', + }, + }, + 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: 'name' }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'email', + }, + }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'Users' }, + }, + 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: 'avatar', + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { + kind: 'Name', + value: 'url', + }, + }, + ], + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'firstName', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'lastName', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'nickname', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'email', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'additionalAuthorsCollection', + }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'limit' }, + value: { kind: 'IntValue', value: '20' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'items' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { + kind: 'Name', + value: 'ExternalAuthors', + }, + }, + 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: 'name' }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'email', + }, + }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'Users' }, + }, + 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: 'avatar', + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { + kind: 'Name', + value: 'url', + }, + }, + ], + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'firstName', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'lastName', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'nickname', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'email', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'correspondingAuthorCollection', + }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'limit' }, + value: { kind: 'IntValue', value: '20' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'items' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: '__typename' }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { + kind: 'Name', + value: 'ExternalAuthors', + }, + }, + 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: 'name' }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'email', + }, + }, + ], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'Users' }, + }, + 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: 'avatar', + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { + kind: 'Name', + value: 'url', + }, + }, + ], + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'firstName', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'lastName', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'nickname', + }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'email', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, { kind: 'Field', name: { kind: 'Name', value: 'linkedFrom' }, diff --git a/packages/contentful/src/crn/queries/manuscript.queries.ts b/packages/contentful/src/crn/queries/manuscript.queries.ts index f04d401a80..07e15f4487 100644 --- a/packages/contentful/src/crn/queries/manuscript.queries.ts +++ b/packages/contentful/src/crn/queries/manuscript.queries.ts @@ -121,6 +121,100 @@ export const manuscriptContentQueryFragment = gql` } } } + updatedBy { + sys { + id + } + firstName + nickname + lastName + alumniSinceDate + avatar { + url + } + teamsCollection(limit: 3) { + items { + team { + sys { + id + } + displayName + } + } + } + } + firstAuthorsCollection(limit: 15) { + items { + __typename + ... on ExternalAuthors { + sys { + id + } + name + email + } + ... on Users { + sys { + id + } + avatar { + url + } + firstName + lastName + nickname + email + } + } + } + additionalAuthorsCollection(limit: 15) { + items { + __typename + ... on ExternalAuthors { + sys { + id + } + name + email + } + ... on Users { + sys { + id + } + avatar { + url + } + firstName + lastName + nickname + email + } + } + } + correspondingAuthorCollection(limit: 1) { + items { + __typename + ... on ExternalAuthors { + sys { + id + } + name + email + } + ... on Users { + sys { + id + } + avatar { + url + } + firstName + lastName + nickname + email + } + } + } linkedFrom { complianceReportsCollection(limit: 1) { items { diff --git a/packages/contentful/src/crn/schema/autogenerated-schema.graphql b/packages/contentful/src/crn/schema/autogenerated-schema.graphql index 603fa5e9af..5529ffd3a4 100644 --- a/packages/contentful/src/crn/schema/autogenerated-schema.graphql +++ b/packages/contentful/src/crn/schema/autogenerated-schema.graphql @@ -2820,6 +2820,7 @@ type ManuscriptVersions implements Entry & _Node { sys: Sys! teamsCollection(limit: Int = 100, locale: String, order: [ManuscriptVersionsTeamsCollectionOrder], preview: Boolean, skip: Int = 0, where: TeamsFilter): ManuscriptVersionsTeamsCollection type(locale: String): String + updatedBy(locale: String, preview: Boolean, where: UsersFilter): Users } type ManuscriptVersionsAdditionalAuthorsCollection { @@ -3055,6 +3056,8 @@ input ManuscriptVersionsFilter { type_not: String type_not_contains: String type_not_in: [String] + updatedBy: cfUsersNestedFilter + updatedBy_exists: Boolean } type ManuscriptVersionsFirstAuthorsCollection { @@ -8048,6 +8051,7 @@ input cfManuscriptVersionsNestedFilter { type_not: String type_not_contains: String type_not_in: [String] + updatedBy_exists: Boolean } input cfMessagesNestedFilter { diff --git a/packages/fixtures/src/manuscripts.ts b/packages/fixtures/src/manuscripts.ts index 03287dfd56..1abf1b72e4 100644 --- a/packages/fixtures/src/manuscripts.ts +++ b/packages/fixtures/src/manuscripts.ts @@ -31,6 +31,7 @@ export const createManuscriptResponse = ( type: 'Original Research', description: 'A good description', createdBy: manuscriptAuthor, + updatedBy: manuscriptAuthor, createdDate: '2020-12-10T20:36:54Z', publishedAt: '2020-12-10T20:36:54Z', manuscriptFile: { @@ -57,6 +58,9 @@ export const createManuscriptResponse = ( }, ], labs: [{ name: 'Lab 1', id: 'lab-1' }], + firstAuthors: [], + correspondingAuthor: [], + additionalAuthors: [], }, ], }); diff --git a/packages/model/src/external-author.ts b/packages/model/src/external-author.ts index 2ef81ac428..6cc56d33a4 100644 --- a/packages/model/src/external-author.ts +++ b/packages/model/src/external-author.ts @@ -4,7 +4,9 @@ import { UserResponse } from './user'; export type ExternalAuthorDataObject = Pick< UserResponse, 'id' | 'displayName' | 'orcid' ->; +> & { + email?: string; +}; export type ListExternalAuthorDataObject = ListResponse; diff --git a/packages/model/src/manuscript.ts b/packages/model/src/manuscript.ts index 0979adcf83..0711ade47a 100644 --- a/packages/model/src/manuscript.ts +++ b/packages/model/src/manuscript.ts @@ -1,5 +1,5 @@ import { JSONSchemaType } from 'ajv'; -import { AuthorAlgoliaResponse } from './authors'; +import { AuthorAlgoliaResponse, AuthorResponse } from './authors'; import { ComplianceReportDataObject } from './compliance-report'; import { DiscussionDataObject } from './discussion'; import { UserResponse } from './user'; @@ -113,9 +113,23 @@ export type ManuscriptVersion = { > & { teams: { id: string; name: string }[]; }; + updatedBy: Pick< + UserResponse, + | 'id' + | 'firstName' + | 'lastName' + | 'displayName' + | 'avatarUrl' + | 'alumniSinceDate' + > & { + teams: { id: string; name: string }[]; + }; createdDate: string; publishedAt: string; complianceReport?: ComplianceReportDataObject; + firstAuthors: AuthorResponse[]; + correspondingAuthor: AuthorResponse[]; + additionalAuthors: AuthorResponse[]; }; export const manuscriptFormFieldsMapping: Record< @@ -125,7 +139,15 @@ export const manuscriptFormFieldsMapping: Record< Array< keyof Omit< ManuscriptVersion, - 'complianceReport' | 'createdBy' | 'createdDate' | 'id' | 'publishedAt' + | 'complianceReport' + | 'createdBy' + | 'createdDate' + | 'id' + | 'publishedAt' + | 'updatedBy' + | 'firstAuthors' + | 'correspondingAuthor' + | 'additionalAuthors' > > > @@ -338,8 +360,26 @@ export type ManuscriptPostRequest = Pick< }[]; }; -export type ManuscriptPutRequest = Pick; -export type ManuscriptUpdateDataObject = ManuscriptPutRequest; +export type ManuscriptUpdateStatus = Pick; +export type ManuscriptUpdateContent = Partial; +export type ManuscriptPutRequest = + | ManuscriptUpdateStatus + | ManuscriptUpdateContent; + +export type ManuscriptUpdateDataObject = + | ManuscriptUpdateStatus + | Partial< + Omit & { + versions: (Omit< + ManuscriptPostRequest['versions'][number], + 'firstAuthors' | 'correspondingAuthor' | 'additionalAuthors' + > & { + firstAuthors: string[]; + correspondingAuthor: string[]; + additionalAuthors: string[]; + })[]; + } + >; type MultiselectOption = { label: string; @@ -423,6 +463,155 @@ export type ManuscriptCreateDataObject = Omit< })[]; }; +export const manuscriptVersionSchema = { + type: 'object', + properties: { + type: { enum: manuscriptTypes, type: 'string' }, + lifecycle: { enum: manuscriptLifecycles, type: 'string' }, + preprintDoi: { type: 'string', nullable: true }, + publicationDoi: { type: 'string', nullable: true }, + requestingApcCoverage: { + enum: apcCoverageOptions, + type: 'string', + nullable: true, + }, + submitterName: { type: 'string', nullable: true }, + submissionDate: { + type: 'string', + format: 'date-time', + nullable: true, + }, + + otherDetails: { type: 'string', nullable: true }, + description: { type: 'string' }, + manuscriptFile: { + type: 'object', + properties: { + id: { type: 'string' }, + filename: { type: 'string', nullable: true }, + url: { type: 'string', nullable: true }, + }, + nullable: true, + required: ['id'], + }, + keyResourceTable: { + type: 'object', + properties: { + id: { type: 'string' }, + filename: { type: 'string', nullable: true }, + url: { type: 'string', nullable: true }, + }, + nullable: true, + required: ['id'], + }, + additionalFiles: { + type: 'array', + nullable: true, + items: { + type: 'object', + additionalProperties: false, + properties: { + id: { type: 'string' }, + filename: { type: 'string', nullable: true }, + url: { type: 'string', nullable: true }, + }, + required: ['id'], + }, + }, + acknowledgedGrantNumber: { type: 'string', nullable: true }, + asapAffiliationIncluded: { type: 'string', nullable: true }, + manuscriptLicense: { type: 'string', nullable: true }, + datasetsDeposited: { type: 'string', nullable: true }, + codeDeposited: { type: 'string', nullable: true }, + protocolsDeposited: { type: 'string', nullable: true }, + labMaterialsRegistered: { type: 'string', nullable: true }, + availabilityStatement: { type: 'string', nullable: true }, + acknowledgedGrantNumberDetails: { type: 'string', nullable: true }, + asapAffiliationIncludedDetails: { type: 'string', nullable: true }, + manuscriptLicenseDetails: { type: 'string', nullable: true }, + datasetsDepositedDetails: { type: 'string', nullable: true }, + codeDepositedDetails: { type: 'string', nullable: true }, + protocolsDepositedDetails: { type: 'string', nullable: true }, + labMaterialsRegisteredDetails: { type: 'string', nullable: true }, + availabilityStatementDetails: { type: 'string', nullable: true }, + + teams: { type: 'array', minItems: 1, items: { type: 'string' } }, + labs: { type: 'array', nullable: true, items: { type: 'string' } }, + firstAuthors: { + type: 'array', + items: { + oneOf: [ + { + type: 'object', + properties: { + userId: { type: 'string' }, + }, + required: ['userId'], + }, + { + type: 'object', + properties: { + externalAuthorId: { type: 'string', nullable: true }, + externalAuthorName: { type: 'string' }, + externalAuthorEmail: { type: 'string' }, + }, + required: ['externalAuthorName', 'externalAuthorEmail'], + }, + ], + }, + }, + correspondingAuthor: { + type: 'object', + nullable: true, + oneOf: [ + { + type: 'object', + properties: { + userId: { type: 'string' }, + }, + required: ['userId'], + }, + { + type: 'object', + properties: { + externalAuthorId: { type: 'string', nullable: true }, + externalAuthorName: { type: 'string' }, + externalAuthorEmail: { type: 'string' }, + }, + required: ['externalAuthorName', 'externalAuthorEmail'], + }, + ], + }, + additionalAuthors: { + type: 'array', + nullable: true, + + items: { + oneOf: [ + { + type: 'object', + properties: { + userId: { type: 'string' }, + }, + required: ['userId'], + }, + { + type: 'object', + properties: { + externalAuthorId: { type: 'string', nullable: true }, + externalAuthorName: { type: 'string' }, + externalAuthorEmail: { type: 'string' }, + }, + required: ['externalAuthorName', 'externalAuthorEmail'], + }, + ], + }, + }, + }, + required: ['type', 'lifecycle'], + additionalProperties: false, +} as const; + export const manuscriptPostRequestSchema: JSONSchemaType = { type: 'object', @@ -438,154 +627,7 @@ export const manuscriptPostRequestSchema: JSONSchemaType type: 'array', maxItems: 1, minItems: 1, - items: { - type: 'object', - properties: { - type: { enum: manuscriptTypes, type: 'string' }, - lifecycle: { enum: manuscriptLifecycles, type: 'string' }, - preprintDoi: { type: 'string', nullable: true }, - publicationDoi: { type: 'string', nullable: true }, - requestingApcCoverage: { - enum: apcCoverageOptions, - type: 'string', - nullable: true, - }, - submitterName: { type: 'string', nullable: true }, - submissionDate: { - type: 'string', - format: 'date-time', - nullable: true, - }, - - otherDetails: { type: 'string', nullable: true }, - description: { type: 'string' }, - manuscriptFile: { - type: 'object', - properties: { - id: { type: 'string' }, - filename: { type: 'string', nullable: true }, - url: { type: 'string', nullable: true }, - }, - nullable: true, - required: ['id'], - }, - keyResourceTable: { - type: 'object', - properties: { - id: { type: 'string' }, - filename: { type: 'string', nullable: true }, - url: { type: 'string', nullable: true }, - }, - nullable: true, - required: ['id'], - }, - additionalFiles: { - type: 'array', - nullable: true, - items: { - type: 'object', - additionalProperties: false, - properties: { - id: { type: 'string' }, - filename: { type: 'string', nullable: true }, - url: { type: 'string', nullable: true }, - }, - required: ['id'], - }, - }, - acknowledgedGrantNumber: { type: 'string', nullable: true }, - asapAffiliationIncluded: { type: 'string', nullable: true }, - manuscriptLicense: { type: 'string', nullable: true }, - datasetsDeposited: { type: 'string', nullable: true }, - codeDeposited: { type: 'string', nullable: true }, - protocolsDeposited: { type: 'string', nullable: true }, - labMaterialsRegistered: { type: 'string', nullable: true }, - availabilityStatement: { type: 'string', nullable: true }, - acknowledgedGrantNumberDetails: { type: 'string', nullable: true }, - asapAffiliationIncludedDetails: { type: 'string', nullable: true }, - manuscriptLicenseDetails: { type: 'string', nullable: true }, - datasetsDepositedDetails: { type: 'string', nullable: true }, - codeDepositedDetails: { type: 'string', nullable: true }, - protocolsDepositedDetails: { type: 'string', nullable: true }, - labMaterialsRegisteredDetails: { type: 'string', nullable: true }, - availabilityStatementDetails: { type: 'string', nullable: true }, - - teams: { type: 'array', minItems: 1, items: { type: 'string' } }, - labs: { type: 'array', nullable: true, items: { type: 'string' } }, - firstAuthors: { - type: 'array', - items: { - oneOf: [ - { - type: 'object', - properties: { - userId: { type: 'string' }, - }, - required: ['userId'], - }, - { - type: 'object', - properties: { - externalAuthorId: { type: 'string', nullable: true }, - externalAuthorName: { type: 'string' }, - externalAuthorEmail: { type: 'string' }, - }, - required: ['externalAuthorName', 'externalAuthorEmail'], - }, - ], - }, - }, - correspondingAuthor: { - type: 'object', - nullable: true, - oneOf: [ - { - type: 'object', - properties: { - userId: { type: 'string' }, - }, - required: ['userId'], - }, - { - type: 'object', - properties: { - externalAuthorId: { type: 'string', nullable: true }, - externalAuthorName: { type: 'string' }, - externalAuthorEmail: { type: 'string' }, - }, - required: ['externalAuthorName', 'externalAuthorEmail'], - }, - ], - }, - additionalAuthors: { - type: 'array', - nullable: true, - - items: { - oneOf: [ - { - type: 'object', - properties: { - userId: { type: 'string' }, - }, - required: ['userId'], - }, - { - type: 'object', - properties: { - externalAuthorId: { type: 'string', nullable: true }, - externalAuthorName: { type: 'string' }, - externalAuthorEmail: { type: 'string' }, - }, - required: ['externalAuthorName', 'externalAuthorEmail'], - }, - ], - }, - }, - }, - required: ['type', 'lifecycle'], - additionalProperties: false, - }, + items: manuscriptVersionSchema, }, }, required: ['title', 'teamId', 'versions'], @@ -596,7 +638,15 @@ export const manuscriptPutRequestSchema: JSONSchemaType = { type: 'object', properties: { + title: { type: 'string', nullable: true }, + teamId: { type: 'string', nullable: true }, status: { enum: manuscriptStatus, type: 'string', nullable: true }, + versions: { + type: 'array', + maxItems: 1, + items: manuscriptVersionSchema, + nullable: true, + }, }, additionalProperties: false, }; diff --git a/packages/react-components/src/icons/index.ts b/packages/react-components/src/icons/index.ts index 2ff97552f7..74ed71515a 100644 --- a/packages/react-components/src/icons/index.ts +++ b/packages/react-components/src/icons/index.ts @@ -94,6 +94,7 @@ export { default as OrcidSocialIcon } from './orcid-social'; export { default as outlookIcon } from './outlook'; export { default as padlockIcon } from './padlock'; export { default as paperClipIcon } from './paper-clip'; +export { default as PencilIcon } from './pencil'; export { default as PercentageIcon } from './percentage'; export { default as placeholderIcon } from './placeholder'; export { default as plusIcon } from './plus'; diff --git a/packages/react-components/src/icons/pencil.tsx b/packages/react-components/src/icons/pencil.tsx new file mode 100644 index 0000000000..8f05af8996 --- /dev/null +++ b/packages/react-components/src/icons/pencil.tsx @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +import { FC } from 'react'; + +interface PencilIconProps { + readonly color?: string; +} + +const PencilIcon: FC = ({ color = '#4D646B' }) => ( + + + +); + +export default PencilIcon; diff --git a/packages/react-components/src/molecules/LabeledRadioButtonGroup.tsx b/packages/react-components/src/molecules/LabeledRadioButtonGroup.tsx index b9d362e702..424feb169f 100644 --- a/packages/react-components/src/molecules/LabeledRadioButtonGroup.tsx +++ b/packages/react-components/src/molecules/LabeledRadioButtonGroup.tsx @@ -15,6 +15,7 @@ export type LabeledRadioButtonGroupProps = { readonly description?: React.ReactNode; readonly options: ReadonlyArray>; readonly validationMessage?: string; + readonly testId?: string; readonly value: V; readonly onChange?: (newValue: V) => void; @@ -51,10 +52,11 @@ export default function LabeledRadioButtonGroup({ onChange = noop, tooltipText, validationMessage, + testId, }: LabeledRadioButtonGroupProps): ReturnType { const groupName = useRef(uuidV4()); return ( -
+
{title || subtitle || description ? ( {title} diff --git a/packages/react-components/src/organisms/ManuscriptAuthors.tsx b/packages/react-components/src/organisms/ManuscriptAuthors.tsx index 68f381a8d2..eb7189f628 100644 --- a/packages/react-components/src/organisms/ManuscriptAuthors.tsx +++ b/packages/react-components/src/organisms/ManuscriptAuthors.tsx @@ -1,5 +1,5 @@ import { AuthorAlgoliaResponse, ManuscriptFormData } from '@asap-hub/model'; -import { ComponentProps } from 'react'; +import { ComponentProps, useEffect } from 'react'; import { Control, Controller, @@ -48,6 +48,19 @@ const ManuscriptAuthors = ({ control, name: `versions.0.${fieldName}Emails`, }); + + useEffect(() => { + const authors = getValues(`versions.0.${fieldName}`); + (authors || []).forEach(({ author }) => { + if (author && !('firstName' in author) && author.displayName) { + append({ + name: author.displayName || '', + email: author?.email || '', + }); + } + }); + }, [append, getValues, fieldName]); + return ( <> = ({ teamId={teamIdCode} grantId={grantId} manuscriptCount={count} + manuscriptId={id} /> ))} diff --git a/packages/react-components/src/organisms/ManuscriptVersionCard.tsx b/packages/react-components/src/organisms/ManuscriptVersionCard.tsx index 8dd6677f35..b4e9f8fbc4 100644 --- a/packages/react-components/src/organisms/ManuscriptVersionCard.tsx +++ b/packages/react-components/src/organisms/ManuscriptVersionCard.tsx @@ -7,6 +7,7 @@ import { import { network } from '@asap-hub/routing'; import { css } from '@emotion/react'; import { ComponentProps, Suspense, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { article, AssociationList, @@ -21,6 +22,7 @@ import { Loading, minusRectIcon, Pill, + PencilIcon, plusRectIcon, QuickCheckReplyModal, Subtitle, @@ -37,6 +39,7 @@ type ManuscriptVersionCardProps = { grantId: string; teamId: string; manuscriptCount: number; + manuscriptId: string; } & Pick, 'onReplyToDiscussion'> & Pick, 'getDiscussion'>; @@ -66,6 +69,28 @@ const toastContentStyles = css({ paddingTop: rem(15), }); +const titleContainerStyles = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + gap: rem(16), + + [`@media (max-width: ${mobileScreen.max}px)`]: { + flexDirection: 'column', + alignItems: 'flex-start', + gap: rem(8), + }, +}); + +const editIconStyles = css({ + width: 'min-content', + minWidth: 'min-content', + flexGrow: 0, + height: 'min-content', +}); + const fileDividerStyles = css({ display: 'block', margin: `${rem(4)} 0`, @@ -115,6 +140,35 @@ const userContainerStyles = css({ paddingTop: rem(32), }); +const updatedByAndEditContainerStyles = css({ + display: 'flex', + flexDirection: 'row', + gap: rem(16), + + [`@media (max-width: ${mobileScreen.max}px)`]: { + flexDirection: 'column', + }, +}); + +const updatedByContainerStyles = css({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', +}); + +const updatedByTextStyles = css({ + display: 'inline-flex', + flexWrap: 'wrap', + justifyContent: 'flex-end', + alignSelf: 'flex-end', + gap: rem(2), + + [`@media (max-width: ${mobileScreen.max}px)`]: { + justifyContent: 'flex-start', + alignSelf: 'flex-start', + }, +}); + const hasAdditionalInfo = (version: ManuscriptVersion) => version.preprintDoi || version.publicationDoi || @@ -170,7 +224,9 @@ const ManuscriptVersionCard: React.FC = ({ manuscriptCount, onReplyToDiscussion, getDiscussion, + manuscriptId, }) => { + const history = useHistory(); const [expanded, setExpanded] = useState(false); const quickCheckDetails = quickCheckQuestions.filter( @@ -179,12 +235,48 @@ const ManuscriptVersionCard: React.FC = ({ const getUserHref = (id: string) => network({}).users({}).user({ userId: id }).$; + const getTeams = (teams: Message['createdBy']['teams']) => teams.map((team) => ({ href: network({}).teams({}).team({ teamId: team.id }).$, name: team.name, })); + const getUpdatedByData = () => { + if ( + version.updatedBy && + version.updatedBy.id && + version.updatedBy.teams && + version.updatedBy.teams.length + ) { + return { + displayName: version.updatedBy.displayName, + userHref: getUserHref(version.updatedBy.id), + teams: getTeams(version.updatedBy.teams), + }; + } + return { + displayName: version.createdBy.displayName, + userHref: getUserHref(version.createdBy.id), + teams: getTeams(version.createdBy.teams), + }; + }; + + const updatedByData = getUpdatedByData(); + + const editManuscriptRoute = + version.createdBy?.teams[0]?.id && + network({}) + .teams({}) + .team({ teamId: version.createdBy.teams[0].id }) + .workspace({}) + .editManuscript({ manuscriptId }).$; + + const handleEditManuscript = () => { + if (editManuscriptRoute) { + history.push(editManuscriptRoute); + } + }; return ( <>
@@ -193,17 +285,47 @@ const ManuscriptVersionCard: React.FC = ({ )}
- + - {article} - Manuscript + {article} +
+ Manuscript + +
+ +
+ + Last Updated: + {formatDate(new Date(version.publishedAt))} + + + Updated by: + + +
+ + +
+
= ({ )}
- Date added: + Date created: {formatDate(new Date(version.createdDate))} ยท - Submitted by: + Created by: { + render( + , + ); + + const emailInput = screen.getByRole('textbox', { name: /Jane Doe Email/i }); + expect(emailInput).toBeInTheDocument(); + expect(emailInput).toHaveValue('jane@doe.com'); + }, +); + +it.each([true, false])( + 'displays pre populated external author email input field without email value when isMultiSelect is %s', + async (isMultiSelect) => { + render( + , + ); + + const emailInput = screen.getByRole('textbox', { name: /Jane Doe Email/i }); + expect(emailInput).toBeInTheDocument(); + expect(emailInput).toHaveValue(''); + }, +); + it.each([true, false])( 'displays external author email input field for a non existing external author when isMultiSelect is %s', async (isMultiSelect) => { diff --git a/packages/react-components/src/organisms/__tests__/ManuscriptVersionCard.test.tsx b/packages/react-components/src/organisms/__tests__/ManuscriptVersionCard.test.tsx index c6ef15896f..175d0ebc4a 100644 --- a/packages/react-components/src/organisms/__tests__/ManuscriptVersionCard.test.tsx +++ b/packages/react-components/src/organisms/__tests__/ManuscriptVersionCard.test.tsx @@ -5,7 +5,9 @@ import { import { ManuscriptLifecycle, ManuscriptVersion } from '@asap-hub/model'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { createMemoryHistory } from 'history'; import React, { ComponentProps } from 'react'; +import { Router } from 'react-router-dom'; import ManuscriptVersionCard, { getLifecycleCode, getManuscriptVersionUID, @@ -35,20 +37,21 @@ const props: ComponentProps = { manuscriptCount: 1, onReplyToDiscussion: jest.fn(), getDiscussion: jest.fn(), + manuscriptId: 'manuscript-1', }; it('displays quick checks when present', () => { const asapAffiliationIncludedDetails = "Including ASAP as an affiliation hasn't been done due to compliance with journal guidelines, needing agreement from authors and institutions, administrative complexities, and balancing recognition with primary affiliations."; const commenter = { - id: 'user-2', - firstName: 'John', - lastName: 'Doe', - displayName: 'John Doe', + id: 'commenter-id', + firstName: 'Connor', + lastName: 'Commenter', + displayName: 'Connor Commenter', teams: [ { - id: 'team-b', - name: 'Team B', + id: 'team-commenter', + name: 'Team Commenter', }, ], }; @@ -63,10 +66,38 @@ it('displays quick checks when present', () => { const getDiscussion = jest.fn(); getDiscussion.mockReturnValueOnce(asapAffiliationIncludedDiscussion); - const { getByText, queryByText, getByRole, rerender, getAllByText } = render( - , - ); - userEvent.click(getByRole('button')); + const author = { + id: 'author-id', + displayName: 'Arthur Author', + firstName: 'Arthur', + lastName: 'Author', + alumniSinceDate: undefined, + avatarUrl: 'http://image', + teams: [ + { + id: 'team-author', + name: 'Team Author', + }, + ], + }; + + const editor = { + id: 'editor-id', + displayName: 'Edith Editor', + firstName: 'Edith', + lastName: 'Editor', + alumniSinceDate: undefined, + avatarUrl: 'http://image', + teams: [ + { + id: 'team-editor', + name: 'Team Editor', + }, + ], + }; + const { getByText, queryByText, getByLabelText, rerender, getAllByText } = + render(); + userEvent.click(getByLabelText('Expand Version')); expect( queryByText( @@ -77,18 +108,8 @@ it('displays quick checks when present', () => { const updatedVersion = { ...baseVersion, asapAffiliationIncludedDetails: asapAffiliationIncludedDiscussion, - createdBy: { - id: 'user-1', - firstName: 'Joe', - lastName: 'Doe', - displayName: 'Joe Doe', - teams: [ - { - id: 'team-a', - name: 'Team A', - }, - ], - }, + createdBy: author, + updatedBy: editor, createdDate: '2024-06-20T11:06:58.899Z', publishedAt: '2024-06-21T11:06:58.899Z', otherDetails: 'Necessary info', @@ -113,33 +134,90 @@ it('displays quick checks when present', () => { ), ).toBeVisible(); - expect(getAllByText('Joe Doe').length).toEqual(1); - expect(getAllByText('John Doe').length).toEqual(1); - expect(getAllByText('Team A').length).toEqual(1); - expect(getAllByText('Team B').length).toEqual(1); - expect(getAllByText('21st June 2024').length).toEqual(1); + expect(getAllByText('Arthur Author').length).toEqual(1); + expect(getAllByText('Edith Editor').length).toEqual(1); + expect(getAllByText('Connor Commenter').length).toEqual(1); + expect(getAllByText('Team Author').length).toEqual(1); + expect(getAllByText('Team Editor').length).toEqual(1); + expect(getAllByText('Team Commenter').length).toEqual(1); + expect(getAllByText('21st June 2024').length).toEqual(2); expect(getAllByText('20th June 2024').length).toEqual(1); - expect(getAllByText('Joe Doe')[0]!.closest('a')!.href!).toContain( - '/network/users/user-1', + expect(getAllByText('Arthur Author')[0]!.closest('a')!.href!).toContain( + '/network/users/author-id', ); - expect(getAllByText('Team A')[0]!.closest('a')!.href!).toContain( - '/network/teams/team-a', + expect(getAllByText('Team Author')[0]!.closest('a')!.href!).toContain( + '/network/teams/team-author', ); - expect(getAllByText('John Doe')[0]!.closest('a')!.href!).toContain( - '/network/users/user-2', + expect(getAllByText('Connor Commenter')[0]!.closest('a')!.href!).toContain( + '/network/users/commenter-id', ); - expect(getAllByText('Team B')[0]!.closest('a')!.href!).toContain( - '/network/teams/team-b', + expect(getAllByText('Team Commenter')[0]!.closest('a')!.href!).toContain( + '/network/teams/team-commenter', + ); +}); +it('displays createdBy as fallback for updatedBy when updatedBy is well defined', () => { + const author = { + id: 'author-id', + displayName: 'Arthur Author', + firstName: 'Arthur', + lastName: 'Author', + alumniSinceDate: undefined, + avatarUrl: 'http://image', + teams: [ + { + id: 'team-author', + name: 'Team Author', + }, + ], + }; + + const screen = render( + , + ); + + userEvent.click(screen.getByLabelText('Expand Version')); + + expect(screen.getAllByText('Arthur Author').length).toEqual(2); + expect( + screen.getAllByText('Arthur Author')[0]!.closest('a')!.href!, + ).toContain('/network/users/author-id'); + expect(screen.getAllByText('Team Author')[0]!.closest('a')!.href!).toContain( + '/network/teams/team-author', + ); +}); + +it('navigates to edit form page when clicking on edit button', () => { + const history = createMemoryHistory(); + const pushSpy = jest.spyOn(history, 'push'); + + const { getByLabelText } = render( + + + , + ); + userEvent.click(getByLabelText('Edit')); + expect(pushSpy).toHaveBeenCalledWith( + '/network/teams/team-id-0/workspace/edit-manuscript/manuscript-1', ); }); it('displays Additional Information section when present', () => { - const { getByRole, queryByRole, rerender } = render( + const { getByRole, queryByRole, rerender, getByLabelText } = render( , ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect( queryByRole('heading', { name: /Additional Information/i }), ).not.toBeInTheDocument(); @@ -162,7 +240,7 @@ it('displays Additional Information section when present', () => { }); it('renders a divider between fields in Additional Information section and files section', () => { - const { getByRole, queryAllByRole } = render( + const { queryAllByRole, getByLabelText } = render( , ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect(queryAllByRole('separator').length).toEqual(6); }); @@ -186,10 +264,10 @@ it.each` ${'requestingApcCoverage'} | ${'Requested APC Coverage?'} | ${'Yes'} ${'otherDetails'} | ${'Other details'} | ${'new details'} `(`displays field $field when present`, async ({ field, title, newValue }) => { - const { getByRole, getByText, queryByText, rerender } = render( + const { getByLabelText, getByText, queryByText, rerender } = render( , ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect(queryByText(title)).not.toBeInTheDocument(); const updatedVersion = { @@ -213,7 +291,7 @@ it('builds the correct href for doi fields', () => { `https://doi.org/${publicationDoiValue}`, ).toString(); - const { getByText, getByRole } = render( + const { getByText, getByLabelText } = render( { }} />, ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect(getByText(preprintDoiValue)?.closest('a')).toHaveAttribute( 'href', @@ -236,7 +314,7 @@ it('builds the correct href for doi fields', () => { }); it('renders manuscript main file details and download link', () => { - const { getByText, getByRole } = render( + const { getByText, getByLabelText } = render( { }} />, ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect(getByText('manuscript_file.pdf')).toBeVisible(); expect(getByText('Download').closest('a')).toHaveAttribute( @@ -260,7 +338,7 @@ it('renders manuscript main file details and download link', () => { }); it('renders key resource table file details and download link', () => { - const { getAllByText, getByText, getByRole } = render( + const { getAllByText, getByText, getByLabelText } = render( { }} />, ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect(getByText('key_resource_table.csv')).toBeVisible(); expect(getAllByText('Download')[1]!.closest('a')).toHaveAttribute( @@ -288,7 +366,7 @@ it('renders key resource table file details and download link', () => { }); it("does not display Submitter's Name and Submission Date if submitterName and submissionDate are not defined", () => { - const { getByRole, getByText, queryByText } = render( + const { getByLabelText, getByText, queryByText } = render( , ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect(getByText(/Requested APC Coverage/i)).toBeInTheDocument(); expect(getByText(/No/i)).toBeInTheDocument(); @@ -310,7 +388,7 @@ it("does not display Submitter's Name and Submission Date if submitterName and s }); it('displays apc coverage information', () => { - const { getByRole, getByText } = render( + const { getByLabelText, getByText } = render( { }} />, ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect(getByText(/Requested APC Coverage/i)).toBeInTheDocument(); expect(getByText(/Already submitted/i)).toBeInTheDocument(); @@ -334,7 +412,7 @@ it('displays apc coverage information', () => { }); it('renders additional files details and download link when provided', () => { - const { getAllByText, getByText, getByRole } = render( + const { getAllByText, getByText, getByLabelText } = render( { }} />, ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect(getByText('additional_file.pdf')).toBeVisible(); expect(getAllByText('Download')[1]!.closest('a')).toHaveAttribute( @@ -365,10 +443,10 @@ it('renders additional files details and download link when provided', () => { }); it('displays compliance report section when present', () => { - const { getByRole, queryByRole, rerender } = render( + const { getByLabelText, queryByRole, rerender, getByRole } = render( , ); - userEvent.click(getByRole('button')); + userEvent.click(getByLabelText('Expand Version')); expect( queryByRole('heading', { name: /Compliance Report/i }), ).not.toBeInTheDocument(); @@ -394,16 +472,17 @@ it('displays manuscript description', () => { const longDescription = 'A veeery long description.'.repeat(200); setScrollHeightMock(100); - const { getByRole, rerender, getByText, queryByRole } = render( - , - ); - userEvent.click(getByRole('button')); + const { getByRole, rerender, getByText, queryByRole, getByLabelText } = + render( + , + ); + userEvent.click(getByLabelText('Expand Version')); expect(getByText(shortDescription)).toBeInTheDocument(); expect(queryByRole('button', { name: /show more/i })).not.toBeInTheDocument(); diff --git a/packages/react-components/src/templates/ManuscriptForm.tsx b/packages/react-components/src/templates/ManuscriptForm.tsx index 31b035bce5..3597af2712 100644 --- a/packages/react-components/src/templates/ManuscriptForm.tsx +++ b/packages/react-components/src/templates/ManuscriptForm.tsx @@ -9,6 +9,7 @@ import { ManuscriptLifecycle, ManuscriptPostAuthor, ManuscriptPostRequest, + ManuscriptPutRequest, ManuscriptResponse, ManuscriptType, manuscriptTypeLifecycles, @@ -90,8 +91,12 @@ type OptionalVersionFields = Array< | 'lifecycle' | 'complianceReport' | 'createdBy' + | 'updatedBy' | 'createdDate' | 'publishedAt' + | 'firstAuthors' + | 'correspondingAuthor' + | 'additionalAuthors' > >; @@ -235,6 +240,7 @@ type ManuscriptFormProps = Omit< | 'additionalFiles' | 'description' | 'createdBy' + | 'updatedBy' | 'createdDate' | 'publishedAt' | 'teams' @@ -248,9 +254,14 @@ type ManuscriptFormProps = Omit< additionalFiles?: ManuscriptFileResponse[]; description?: string | ''; eligibilityReasons: Set; - onSave: ( + onCreate: ( output: ManuscriptPostRequest, ) => Promise; + onUpdate: ( + id: string, + output: ManuscriptPutRequest, + ) => Promise; + manuscriptId?: string; onSuccess: () => void; handleFileUpload: ( file: File, @@ -262,6 +273,7 @@ type ManuscriptFormProps = Omit< typeof LabeledMultiSelect >['loadOptions']; selectedTeams: MultiSelectOptionsType[]; + selectedLabs: MultiSelectOptionsType[]; getLabSuggestions?: ComponentProps< typeof LabeledMultiSelect >['loadOptions']; @@ -274,7 +286,9 @@ type ManuscriptFormProps = Omit< }; const ManuscriptForm: React.FC = ({ - onSave, + manuscriptId, + onCreate, + onUpdate, onSuccess, handleFileUpload, teamId, @@ -291,14 +305,6 @@ const ManuscriptForm: React.FC = ({ preprintDoi, publicationDoi, otherDetails, - acknowledgedGrantNumber, - asapAffiliationIncluded, - manuscriptLicense, - datasetsDeposited, - codeDeposited, - protocolsDeposited, - labMaterialsRegistered, - availabilityStatement, acknowledgedGrantNumberDetails, asapAffiliationIncludedDetails, manuscriptLicenseDetails, @@ -310,6 +316,7 @@ const ManuscriptForm: React.FC = ({ getTeamSuggestions, selectedTeams, getLabSuggestions, + selectedLabs, getAuthorSuggestions, description, firstAuthors, @@ -318,6 +325,16 @@ const ManuscriptForm: React.FC = ({ }) => { const history = useHistory(); + const getDefaultQuickCheckValue = (quickCheckDetails: string | undefined) => { + const isEditing = !!title; + + if (isEditing) { + return quickCheckDetails ? 'No' : 'Yes'; + } + + return undefined; + }; + const methods = useForm({ mode: 'onBlur', defaultValues: { @@ -329,20 +346,37 @@ const ManuscriptForm: React.FC = ({ preprintDoi: preprintDoi || '', requestingApcCoverage: requestingApcCoverage || '', submitterName: submitterName || undefined, - submissionDate: submissionDate || undefined, + submissionDate: submissionDate ? new Date(submissionDate) : undefined, publicationDoi: publicationDoi || '', otherDetails: otherDetails || '', manuscriptFile: manuscriptFile || undefined, keyResourceTable: keyResourceTable || undefined, additionalFiles: additionalFiles || undefined, - acknowledgedGrantNumber: acknowledgedGrantNumber || '', - asapAffiliationIncluded: asapAffiliationIncluded || '', - manuscriptLicense: manuscriptLicense || '', - datasetsDeposited: datasetsDeposited || '', - codeDeposited: codeDeposited || '', - protocolsDeposited: protocolsDeposited || '', - labMaterialsRegistered: labMaterialsRegistered || '', - availabilityStatement: availabilityStatement || '', + + acknowledgedGrantNumber: getDefaultQuickCheckValue( + acknowledgedGrantNumberDetails?.message.text, + ), + asapAffiliationIncluded: getDefaultQuickCheckValue( + asapAffiliationIncludedDetails?.message.text, + ), + manuscriptLicense: getDefaultQuickCheckValue( + manuscriptLicenseDetails?.message.text, + ), + datasetsDeposited: getDefaultQuickCheckValue( + datasetsDepositedDetails?.message.text, + ), + codeDeposited: getDefaultQuickCheckValue( + codeDepositedDetails?.message.text, + ), + protocolsDeposited: getDefaultQuickCheckValue( + protocolsDepositedDetails?.message.text, + ), + labMaterialsRegistered: getDefaultQuickCheckValue( + labMaterialsRegisteredDetails?.message.text, + ), + availabilityStatement: getDefaultQuickCheckValue( + availabilityStatementDetails?.message.text, + ), acknowledgedGrantNumberDetails: acknowledgedGrantNumberDetails?.message.text || '', asapAffiliationIncludedDetails: @@ -359,6 +393,7 @@ const ManuscriptForm: React.FC = ({ availabilityStatementDetails: availabilityStatementDetails?.message.text || '', teams: selectedTeams || [], + labs: selectedLabs || [], description: description || '', firstAuthors: firstAuthors || [], firstAuthorsEmails: [], @@ -429,7 +464,7 @@ const ManuscriptForm: React.FC = ({ ...getValues().versions[0], ...fieldDefaultValueMap, teams: selectedTeams, - labs: [], + labs: selectedLabs, manuscriptFile: undefined, keyResourceTable: undefined, additionalFiles: undefined, @@ -441,6 +476,8 @@ const ManuscriptForm: React.FC = ({ { keepDefaultValues: true }, ); } + // TODO: when edit remove reset? + // eslint-disable-next-line react-hooks/exhaustive-deps }, [getValues, reset, watchType, watchLifecycle, selectedTeams]); const onSubmit = async (data: ManuscriptFormData) => { @@ -451,96 +488,109 @@ const ManuscriptForm: React.FC = ({ firstAuthorsEmails, correspondingAuthorEmails, additionalAuthorsEmails, - ...postRequestVersionData + ...requestVersionData } = versionData; - await onSave({ - ...data, - teamId, - eligibilityReasons: [...eligibilityReasons], - versions: [ - { - ...postRequestVersionData, - publicationDoi: versionData?.publicationDoi || undefined, - preprintDoi: versionData?.preprintDoi || undefined, - otherDetails: versionData?.otherDetails || undefined, - requestingApcCoverage: - versionData?.requestingApcCoverage || undefined, - description: versionData.description || '', - submitterName: - versionData?.requestingApcCoverage === 'Already submitted' && - versionData?.submitterName - ? versionData.submitterName - : undefined, - submissionDate: - versionData?.requestingApcCoverage === 'Already submitted' && - versionData.submissionDate - ? versionData.submissionDate.toISOString() - : undefined, - - acknowledgedGrantNumber: - versionData.acknowledgedGrantNumber || undefined, - asapAffiliationIncluded: - versionData.asapAffiliationIncluded || undefined, - availabilityStatement: - versionData.availabilityStatement || undefined, - manuscriptLicense: versionData.manuscriptLicense || undefined, - datasetsDeposited: versionData.datasetsDeposited || undefined, - codeDeposited: versionData.codeDeposited || undefined, - protocolsDeposited: versionData.protocolsDeposited || undefined, - labMaterialsRegistered: - versionData.labMaterialsRegistered || undefined, - - acknowledgedGrantNumberDetails: - versionData?.acknowledgedGrantNumber === 'No' - ? versionData.acknowledgedGrantNumberDetails - : '', - asapAffiliationIncludedDetails: - versionData?.asapAffiliationIncluded === 'No' - ? versionData.asapAffiliationIncludedDetails - : '', - availabilityStatementDetails: - versionData?.availabilityStatement === 'No' - ? versionData.availabilityStatementDetails - : '', - manuscriptLicenseDetails: - versionData?.manuscriptLicense === 'No' - ? versionData.manuscriptLicenseDetails - : '', - datasetsDepositedDetails: - versionData?.datasetsDeposited === 'No' - ? versionData.datasetsDepositedDetails - : '', - codeDepositedDetails: - versionData?.codeDeposited === 'No' - ? versionData.codeDepositedDetails - : '', - protocolsDepositedDetails: - versionData?.protocolsDeposited === 'No' - ? versionData.protocolsDepositedDetails - : '', - labMaterialsRegisteredDetails: - versionData?.labMaterialsRegistered === 'No' - ? versionData.labMaterialsRegisteredDetails - : '', - - teams: versionData.teams.map((team) => team.value), - labs: versionData.labs.map((lab) => lab.value), - firstAuthors: getPostAuthors( - versionData.firstAuthors, - firstAuthorsEmails, - ), - correspondingAuthor: getPostAuthors( - versionData.correspondingAuthor, - correspondingAuthorEmails, - )?.[0], - additionalAuthors: getPostAuthors( - versionData.additionalAuthors, - additionalAuthorsEmails, - ), - }, - ], - }); + const versionDataPayload = { + publicationDoi: versionData?.publicationDoi || undefined, + preprintDoi: versionData?.preprintDoi || undefined, + otherDetails: versionData?.otherDetails || undefined, + requestingApcCoverage: versionData?.requestingApcCoverage || undefined, + description: versionData.description || '', + submitterName: + versionData?.requestingApcCoverage === 'Already submitted' && + versionData?.submitterName + ? versionData.submitterName + : undefined, + submissionDate: + versionData?.requestingApcCoverage === 'Already submitted' && + versionData.submissionDate + ? versionData.submissionDate.toISOString() + : undefined, + + acknowledgedGrantNumber: + versionData.acknowledgedGrantNumber || undefined, + asapAffiliationIncluded: + versionData.asapAffiliationIncluded || undefined, + availabilityStatement: versionData.availabilityStatement || undefined, + manuscriptLicense: versionData.manuscriptLicense || undefined, + datasetsDeposited: versionData.datasetsDeposited || undefined, + codeDeposited: versionData.codeDeposited || undefined, + protocolsDeposited: versionData.protocolsDeposited || undefined, + labMaterialsRegistered: versionData.labMaterialsRegistered || undefined, + + acknowledgedGrantNumberDetails: + versionData?.acknowledgedGrantNumber === 'No' + ? versionData.acknowledgedGrantNumberDetails + : '', + asapAffiliationIncludedDetails: + versionData?.asapAffiliationIncluded === 'No' + ? versionData.asapAffiliationIncludedDetails + : '', + availabilityStatementDetails: + versionData?.availabilityStatement === 'No' + ? versionData.availabilityStatementDetails + : '', + manuscriptLicenseDetails: + versionData?.manuscriptLicense === 'No' + ? versionData.manuscriptLicenseDetails + : '', + datasetsDepositedDetails: + versionData?.datasetsDeposited === 'No' + ? versionData.datasetsDepositedDetails + : '', + codeDepositedDetails: + versionData?.codeDeposited === 'No' + ? versionData.codeDepositedDetails + : '', + protocolsDepositedDetails: + versionData?.protocolsDeposited === 'No' + ? versionData.protocolsDepositedDetails + : '', + labMaterialsRegisteredDetails: + versionData?.labMaterialsRegistered === 'No' + ? versionData.labMaterialsRegisteredDetails + : '', + + teams: versionData.teams.map((team) => team.value), + labs: versionData.labs.map((lab) => lab.value), + firstAuthors: getPostAuthors( + versionData.firstAuthors, + firstAuthorsEmails, + ), + correspondingAuthor: getPostAuthors( + versionData.correspondingAuthor, + correspondingAuthorEmails, + )?.[0], + additionalAuthors: getPostAuthors( + versionData.additionalAuthors, + additionalAuthorsEmails, + ), + }; + if (!manuscriptId) { + await onCreate({ + ...data, + teamId, + eligibilityReasons: [...eligibilityReasons], + versions: [ + { + ...requestVersionData, + ...versionDataPayload, + }, + ], + }); + } else { + await onUpdate(manuscriptId, { + title: data.title, + teamId, + versions: [ + { + ...requestVersionData, + ...versionDataPayload, + }, + ], + }); + } onSuccess(); } @@ -1156,6 +1206,7 @@ const ManuscriptForm: React.FC = ({ fieldState: { error }, }) => ( + testId={field} title={question} subtitle="(required)" description={getQuickCheckDescription(field)} diff --git a/packages/react-components/src/templates/__tests__/ManuscriptForm.test.tsx b/packages/react-components/src/templates/__tests__/ManuscriptForm.test.tsx index d94c3dc99a..e879104836 100644 --- a/packages/react-components/src/templates/__tests__/ManuscriptForm.test.tsx +++ b/packages/react-components/src/templates/__tests__/ManuscriptForm.test.tsx @@ -3,12 +3,15 @@ import { screen, waitFor, waitForElementToBeRemoved, + within, } from '@testing-library/react'; import { ComponentProps } from 'react'; import { MemoryRouter, Route, Router, StaticRouter } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import userEvent, { specialChars } from '@testing-library/user-event'; import { + AuthorResponse, + AuthorSelectOption, ManuscriptFileType, manuscriptFormFieldsMapping, ManuscriptLifecycle, @@ -42,11 +45,14 @@ getTeamSuggestions.mockResolvedValue([ ]); const defaultProps: ComponentProps = { - onSave: jest.fn(() => Promise.resolve()), + manuscriptId: undefined, + onCreate: jest.fn(() => Promise.resolve()), + onUpdate: jest.fn(() => Promise.resolve()), getAuthorSuggestions: jest.fn(), getLabSuggestions: mockGetLabSuggestions, getTeamSuggestions, selectedTeams: [{ value: '1', label: 'One Team', isFixed: true }], + selectedLabs: [], handleFileUpload: jest.fn(() => Promise.resolve({ id: '123', @@ -70,8 +76,12 @@ const defaultProps: ComponentProps = { { label: 'Author 1', value: 'author-1', - }, + id: 'author-1', + displayName: 'Author 1', + } as AuthorResponse & AuthorSelectOption, ], + correspondingAuthor: [], + additionalAuthors: [], submitterName: 'John Doe', submissionDate: new Date('2024-10-01'), }; @@ -89,7 +99,7 @@ it('renders the form', async () => { }); it('data is sent on form submission', async () => { - const onSave = jest.fn(); + const onCreate = jest.fn(); render( { filename: 'test.csv', url: 'http://example.com/test.csv', }} - onSave={onSave} + onCreate={onCreate} /> , ); userEvent.click(screen.getByRole('button', { name: /Submit/ })); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith({ + expect(onCreate).toHaveBeenCalledWith({ title: 'manuscript title', eligibilityReasons: [], versions: [ @@ -181,7 +191,7 @@ test.each` field: QuickCheck; fieldDetails: QuickCheckDetails; }) => { - const onSave = jest.fn(); + const onCreate = jest.fn(); const props = { ...defaultProps, [field]: 'No', @@ -205,7 +215,7 @@ test.each` filename: 'test.csv', url: 'http://example.com/test.csv', }} - onSave={onSave} + onCreate={onCreate} /> , ); @@ -255,7 +265,7 @@ test.each` payload.versions[0]![field] = 'No'; payload.versions[0]![fieldDetails] = 'Explanation'; await waitFor(() => { - expect(onSave).toHaveBeenCalledWith(payload); + expect(onCreate).toHaveBeenCalledWith(payload); }); }, ); @@ -279,7 +289,7 @@ test.each` field: QuickCheck; fieldDetails: QuickCheckDetails; }) => { - const onSave = jest.fn(); + const onCreate = jest.fn(); const props = { ...defaultProps, [field]: 'Yes', @@ -289,7 +299,7 @@ test.each` , ); + userEvent.type( + screen.getByRole('textbox', { name: /Title of Manuscript/i }), + 'manuscript title', + ); + const quickCheckFields = quickCheckQuestions.map((q) => q.field); + + quickCheckFields.forEach((f) => { + within(screen.getByTestId(f)).getByText('Yes').click(); + }); + userEvent.click(screen.getByRole('button', { name: /Submit/ })); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith({ + expect(onCreate).toHaveBeenCalledWith({ title: 'manuscript title', eligibilityReasons: [], versions: [ - { - type: 'Original Research', - lifecycle: 'Publication', - manuscriptFile: expect.anything(), - keyResourceTable: expect.anything(), - publicationDoi: '10.0777', - requestingApcCoverage: 'Already submitted', - submissionDate: '2024-10-01T00:00:00.000Z', - submitterName: 'John Doe', + expect.objectContaining({ acknowledgedGrantNumber: 'Yes', asapAffiliationIncluded: 'Yes', manuscriptLicense: 'Yes', @@ -341,14 +353,7 @@ test.each` protocolsDepositedDetails: '', labMaterialsRegisteredDetails: '', availabilityStatementDetails: '', - - teams: ['1'], - labs: [], - - description: 'Some description', - firstAuthors: [], - additionalAuthors: [], - }, + }), ], teamId, }); @@ -357,16 +362,11 @@ test.each` ); it('displays an error message when user selects no in a quick check and does not provide details', async () => { - const onSave = jest.fn(); - const props = { - ...defaultProps, - acknowledgedGrantNumber: 'No', - acknowledgedGrantNumberDetails: undefined, - }; + const onCreate = jest.fn(); render( , ); @@ -384,6 +384,16 @@ it('displays an error message when user selects no in a quick check and does not screen.queryByText(/Please enter the details./i), ).not.toBeInTheDocument(); + const quickCheckFields = quickCheckQuestions.map((q) => q.field); + + quickCheckFields + .filter((f) => f !== 'acknowledgedGrantNumber') + .forEach((f) => { + within(screen.getByTestId(f)).getByText('Yes').click(); + }); + + within(screen.getByTestId('acknowledgedGrantNumber')).getByText('No').click(); + userEvent.click(screen.getByRole('button', { name: /Submit/ })); await waitFor(() => { @@ -575,7 +585,7 @@ it('displays error message when other details is bigger than 256 characters', as }); it(`sets requestingApcCoverage to 'Already submitted' by default`, async () => { - const onSave = jest.fn(); + const onCreate = jest.fn(); render( { filename: 'test.csv', url: 'http://example.com/test.csv', }} - onSave={onSave} + onCreate={onCreate} /> , ); userEvent.click(screen.getByRole('button', { name: /Submit/ })); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith({ + expect(onCreate).toHaveBeenCalledWith({ title: 'manuscript title', eligibilityReasons: [], teamId, @@ -625,7 +635,7 @@ describe('authors', () => { `( 'submits an existing internal author in $section', async ({ section, submittedValue }) => { - const onSave = jest.fn(); + const onCreate = jest.fn(); const getAuthorSuggestionsMock = jest.fn().mockResolvedValue([ { @@ -648,7 +658,7 @@ describe('authors', () => { { userEvent.click(submitButton); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith( + expect(onCreate).toHaveBeenCalledWith( expect.objectContaining({ versions: [expect.objectContaining(submittedValue)], }), @@ -694,7 +704,7 @@ describe('authors', () => { `( 'submits an existing external author in $section', async ({ section, submittedValue }) => { - const onSave = jest.fn(); + const onCreate = jest.fn(); const getAuthorSuggestionsMock = jest.fn().mockResolvedValue([ { @@ -715,7 +725,7 @@ describe('authors', () => { { userEvent.click(submitButton); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith( + expect(onCreate).toHaveBeenCalledWith( expect.objectContaining({ versions: [expect.objectContaining(submittedValue)], }), @@ -765,7 +775,7 @@ describe('authors', () => { `( 'submits a non existing external author in $section', async ({ section, submittedValue }) => { - const onSave = jest.fn(); + const onCreate = jest.fn(); const getAuthorSuggestionsMock = jest.fn().mockResolvedValue([ { @@ -788,7 +798,7 @@ describe('authors', () => { { userEvent.click(submitButton); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith( + expect(onCreate).toHaveBeenCalledWith( expect.objectContaining({ versions: [expect.objectContaining(submittedValue)], }), @@ -901,7 +911,6 @@ describe('renders the necessary fields', () => { teams: 'Add other teams that contributed to this manuscript.', labs: 'Add ASAP labs that contributed to this manuscript.', - firstAuthors: '', }; describe.each(Object.keys(manuscriptFormFieldsMapping))( @@ -934,13 +943,13 @@ describe('renders the necessary fields', () => { }); it('resets form fields to default values when no longer visible', async () => { - const onSave = jest.fn(); + const onCreate = jest.fn(); render( { userEvent.click(submitButton); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith({ + expect(onCreate).toHaveBeenCalledWith({ title: 'manuscript title', eligibilityReasons: [], versions: [ @@ -1050,10 +1059,10 @@ it('maintains values provided when lifecycle changes but field is still visible' }); it('does not submit when required values are missing', async () => { - const onSave = jest.fn(); + const onCreate = jest.fn(); render( - + , ); @@ -1068,7 +1077,7 @@ it('does not submit when required values are missing', async () => { expect( screen.getByRole('textbox', { name: /Title of Manuscript/i }), ).toBeInvalid(); - expect(onSave).not.toHaveBeenCalled(); + expect(onCreate).not.toHaveBeenCalled(); }); it('should go back when cancel button is clicked', () => { @@ -1336,13 +1345,13 @@ describe('key resource table', () => { describe('additional files', () => { it('user can upload additional files', async () => { - const onSave = jest.fn(); + const onCreate = jest.fn(); render( { }); it('user cannot upload the same file multiple times', async () => { - const onSave = jest.fn(); + const onCreate = jest.fn(); render( { }); it('user can add teams', async () => { - const onSave = jest.fn(); + const onCreate = jest.fn(); const getTeamSuggestionsMock = jest.fn().mockResolvedValue([ { label: 'Team A', value: 'team-a' }, { label: 'Team B', value: 'team-b' }, @@ -1531,7 +1540,7 @@ it('user can add teams', async () => { { userEvent.click(submitButton); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith( + expect(onCreate).toHaveBeenCalledWith( expect.objectContaining({ versions: [ expect.objectContaining({ @@ -1580,7 +1589,7 @@ it('user can add teams', async () => { }); it('user can add labs', async () => { - const onSave = jest.fn(); + const onCreate = jest.fn(); const getLabSuggestions = jest.fn().mockResolvedValue([ { label: 'Lab One', value: 'lab-1' }, { label: 'Lab Two', value: 'lab-2' }, @@ -1590,7 +1599,7 @@ it('user can add labs', async () => { { userEvent.click(submitButton); await waitFor(() => { - expect(onSave).toHaveBeenCalledWith( + expect(onCreate).toHaveBeenCalledWith( expect.objectContaining({ versions: [ expect.objectContaining({ @@ -1662,3 +1671,82 @@ it('displays error message when no lab is found', async () => { await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); expect(screen.getByText(/Sorry, no labs match/i)).toBeVisible(); }); + +it('calls onUpdate when form is updated', async () => { + const onUpdate = jest.fn(); + render( + + + , + ); + + userEvent.click(screen.getByRole('button', { name: /Submit/ })); + await waitFor(() => { + expect(onUpdate).toHaveBeenCalledWith('manuscript-id', { + teamId: '1', + title: 'manuscript title', + versions: [ + { + acknowledgedGrantNumber: 'Yes', + acknowledgedGrantNumberDetails: '', + additionalAuthors: [], + additionalFiles: undefined, + asapAffiliationIncluded: 'Yes', + asapAffiliationIncludedDetails: '', + availabilityStatement: 'Yes', + availabilityStatementDetails: '', + codeDeposited: 'Yes', + codeDepositedDetails: '', + correspondingAuthor: undefined, + datasetsDeposited: 'Yes', + datasetsDepositedDetails: '', + description: 'Some description', + firstAuthors: [], + keyResourceTable: { + filename: 'test.csv', + id: '124', + url: 'http://example.com/test.csv', + }, + labMaterialsRegistered: 'Yes', + labMaterialsRegisteredDetails: '', + labs: [], + lifecycle: 'Draft Manuscript (prior to Publication)', + manuscriptFile: { + filename: 'test.pdf', + id: '123', + url: 'http://example.com/test.pdf', + }, + manuscriptLicense: undefined, + manuscriptLicenseDetails: '', + otherDetails: undefined, + preprintDoi: undefined, + protocolsDeposited: 'Yes', + protocolsDepositedDetails: '', + publicationDoi: undefined, + requestingApcCoverage: undefined, + submissionDate: undefined, + submitterName: undefined, + teams: ['1'], + type: 'Original Research', + }, + ], + }); + }); +}); diff --git a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx index b67f9db436..fb724cdca2 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx @@ -99,6 +99,15 @@ describe('compliance section', () => { }); it('renders type and lifecycle values when expanded', () => { + const user = { + displayName: 'John Doe', + firstName: 'John', + lastName: 'Doe', + id: 'john-doe', + teams: [{ id: 'alessi', name: 'Alessi' }], + avatarUrl: '', + alumniSinceDate: undefined, + }; const teamWithManuscripts: ComponentProps = { ...team, manuscripts: [ @@ -117,15 +126,8 @@ describe('compliance section', () => { filename: 'file.pdf', id: 'file-id', }, - createdBy: { - displayName: 'John Doe', - firstName: 'John', - lastName: 'Doe', - id: 'john-doe', - teams: [{ id: 'alessi', name: 'Alessi' }], - avatarUrl: '', - alumniSinceDate: undefined, - }, + createdBy: user, + updatedBy: user, createdDate: '2020-12-10T20:36:54Z', publishedAt: '2020-12-10T20:36:54Z', teams: [ @@ -141,6 +143,9 @@ describe('compliance section', () => { }, ], labs: [{ name: 'Lab 1', id: 'lab-1' }], + firstAuthors: [], + correspondingAuthor: [], + additionalAuthors: [], }, ], }, @@ -159,15 +164,8 @@ describe('compliance section', () => { filename: 'file.pdf', id: 'file-id', }, - createdBy: { - displayName: 'Jane Doe', - firstName: 'Jane', - lastName: 'Doe', - id: 'jane-doe', - teams: [{ id: 'de-camilli', name: 'De Camilli' }], - avatarUrl: '', - alumniSinceDate: undefined, - }, + createdBy: user, + updatedBy: user, createdDate: '2020-12-10T20:36:54Z', publishedAt: '2020-12-10T20:36:54Z', teams: [ @@ -183,6 +181,9 @@ describe('compliance section', () => { }, ], labs: [{ name: 'Lab 1', id: 'lab-1' }], + firstAuthors: [], + correspondingAuthor: [], + additionalAuthors: [], }, ], }, diff --git a/packages/routing/src/network.ts b/packages/routing/src/network.ts index ead5b330e6..c22b9bfd7e 100644 --- a/packages/routing/src/network.ts +++ b/packages/routing/src/network.ts @@ -92,6 +92,11 @@ const team = (() => { const tool = route('/:toolIndex', { toolIndex: stringParser }, {}); const tools = route('/tools', {}, { tool }); const createManuscript = route('/create-manuscript', {}, {}); + const editManuscript = route( + '/edit-manuscript/:manuscriptId', + { manuscriptId: stringParser }, + {}, + ); const createComplianceReport = route( '/create-compliance-report/:manuscriptId', { manuscriptId: stringParser }, @@ -100,7 +105,7 @@ const team = (() => { const workspace = route( '/workspace', {}, - { tools, createManuscript, createComplianceReport }, + { tools, createManuscript, editManuscript, createComplianceReport }, ); const createOutput = route( '/create-output/:outputDocumentType',