diff --git a/packages/gp2-components/src/organisms/ConfirmAndSaveOutput.tsx b/packages/gp2-components/src/organisms/ConfirmAndSaveOutput.tsx new file mode 100644 index 0000000000..c5306e6eb0 --- /dev/null +++ b/packages/gp2-components/src/organisms/ConfirmAndSaveOutput.tsx @@ -0,0 +1,163 @@ +import { ReactNode, useState } from 'react'; +import { ConfirmModal as Modal, Link } from '@asap-hub/react-components'; +import { + INVITE_SUPPORT_EMAIL, + mailToSupport, +} from '@asap-hub/react-components/src/mail'; +import { gp2 } from '@asap-hub/model'; +import { useNotificationContext } from '@asap-hub/react-context'; + +import { EntityMappper } from '../templates/CreateOutputPage'; +import { GetWrappedOnSave } from './Form'; + +const capitalizeFirstLetter = (string: string) => + string.charAt(0).toUpperCase() + string.slice(1); +export type ConfirmAndSaveOutputProps = { + children: (state: { + save: () => Promise; + }) => ReactNode; + getWrappedOnSave: GetWrappedOnSave; + setRedirectOnSave: (url: string) => void; + path: (id: string) => string; + documentType: gp2.OutputDocumentType; + title: string | undefined; + currentPayload: gp2.OutputPostRequest; + isEditing: boolean; + createVersion: boolean; + shareOutput: ( + payload: gp2.OutputPostRequest, + ) => Promise; + entityType: 'workingGroup' | 'project'; +}; + +const getBannerMessage = ( + entityType: 'workingGroup' | 'project', + documentType: gp2.OutputDocumentType, + published: boolean, + createVersion: boolean, +) => + `${createVersion ? 'New ' : ''}${EntityMappper[entityType]} ${documentType} ${ + createVersion ? 'version ' : '' + }${published || createVersion ? 'published' : 'saved'} successfully.`; + +export const ConfirmAndSaveOutput = ({ + children, + getWrappedOnSave, + setRedirectOnSave, + path, + documentType, + title, + shareOutput, + currentPayload, + createVersion, + isEditing, + entityType, +}: ConfirmAndSaveOutputProps) => { + const [displayPublishModal, setDisplayPublishModal] = useState(false); + const [displayVersionModal, setDiplayVersionModal] = useState(false); + + const { addNotification, removeNotification, notifications } = + useNotificationContext(); + + const setBannerMessage = ( + message: string, + page: 'output' | 'output-form', + bannerType: 'error' | 'success', + ) => { + if ( + notifications[0] && + notifications[0]?.message !== capitalizeFirstLetter(message) + ) { + removeNotification(notifications[0]); + } + addNotification({ + message: capitalizeFirstLetter(message), + page, + type: bannerType, + }); + }; + const save = async (skipConfirmationModal: boolean = false) => { + const displayModalFn = + !isEditing && !skipConfirmationModal + ? () => { + setDisplayPublishModal(true); + } + : createVersion && !skipConfirmationModal + ? () => { + setDiplayVersionModal(true); + } + : null; + + const output = await getWrappedOnSave( + () => shareOutput(currentPayload), + (error) => setBannerMessage(error, 'output-form', 'error'), + displayModalFn, + )(); + + if (output && typeof output.id === 'string') { + setBannerMessage( + getBannerMessage(entityType, documentType, !title, createVersion), + 'output', + 'success', + ); + setRedirectOnSave(path(output.id)); + } + return output; + }; + return ( + <> + {displayPublishModal && ( + setDisplayPublishModal(false)} + confirmText="Publish Output" + onSave={async () => { + const skipPublishModal = true; + const result = await save(skipPublishModal); + if (!result) { + setDisplayPublishModal(false); + } + }} + description={ + <> + All {entityType === 'workingGroup' ? 'working group' : 'project'}{' '} + members listed on this output will be notified and all GP2 members + will be able to access it. If you need to unpublish this output, + please contact{' '} + {{INVITE_SUPPORT_EMAIL}}. + + } + /> + )} + + {displayVersionModal && ( + setDiplayVersionModal(false)} + confirmText="Publish new version" + onSave={async () => { + const skipPublishModal = true; + const result = await save(skipPublishModal); + if (!result) { + setDiplayVersionModal(false); + } + }} + description={ + <> + All working group members listed on this output will be notified + and all GP2 members will be able to access it. If you want to add + or edit older versions after this new version was published, + please contact{' '} + {{INVITE_SUPPORT_EMAIL}}. + + } + /> + )} + {children({ + save, + })} + + ); +}; diff --git a/packages/gp2-components/src/organisms/Form.tsx b/packages/gp2-components/src/organisms/Form.tsx index daa650b94c..7c9777ef49 100644 --- a/packages/gp2-components/src/organisms/Form.tsx +++ b/packages/gp2-components/src/organisms/Form.tsx @@ -14,6 +14,12 @@ const styles = css({ export type FormStatus = 'initial' | 'isSaving' | 'hasError' | 'hasSaved'; +export type GetWrappedOnSave = ( + onSaveFunction: () => Promise, + addNotification: (error: string) => void, + onDisplayModal: (() => void) | null, +) => () => Promise; + type FormProps = { validate?: () => boolean; dirty: boolean; // mandatory so that it cannot be forgotten @@ -21,11 +27,7 @@ type FormProps = { children: (state: { isSaving: boolean; setRedirectOnSave: (url: string) => void; - getWrappedOnSave: ( - onSaveFunction: () => Promise, - addNotification: (error: string) => void, - onDisplayModal: (() => void) | null, - ) => () => Promise; + getWrappedOnSave: GetWrappedOnSave; onCancel: () => void; }) => ReactNode; }; diff --git a/packages/gp2-components/src/organisms/__tests__/ConfirmAndSaveOutput.test.tsx b/packages/gp2-components/src/organisms/__tests__/ConfirmAndSaveOutput.test.tsx new file mode 100644 index 0000000000..6624c108f3 --- /dev/null +++ b/packages/gp2-components/src/organisms/__tests__/ConfirmAndSaveOutput.test.tsx @@ -0,0 +1,151 @@ +import { createOutputResponse } from '@asap-hub/fixtures/src/gp2'; +import { OutputResponse } from '@asap-hub/model/src/gp2'; +import { Button } from '@asap-hub/react-components'; +import { NotificationContext } from '@asap-hub/react-context'; +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; + +import { + ConfirmAndSaveOutput, + ConfirmAndSaveOutputProps, +} from '../ConfirmAndSaveOutput'; +import Form, { GetWrappedOnSave } from '../Form'; + +describe('ConfirmAndSaveOutput', () => { + const addNotification = jest.fn(); + const history = createMemoryHistory(); + const shareOutput = jest.fn(); + + const wrapper: React.ComponentType = ({ children }) => ( + + {children} + + ); + + const renderElement = (props?: Partial) => + render( +
+ {({ getWrappedOnSave }) => ( + id} + documentType="Article" + title="title" + currentPayload={{ + ...createOutputResponse(), + tagIds: [], + contributingCohortIds: [], + mainEntityId: '', + relatedOutputIds: [], + relatedEventIds: [], + authors: [], + }} + shareOutput={shareOutput} + setRedirectOnSave={(url: string) => {}} + entityType="project" + isEditing={false} + createVersion={false} + getWrappedOnSave={ + getWrappedOnSave as unknown as GetWrappedOnSave + } + {...props} + > + {({ save }) => } + + )} +
, + { wrapper }, + ); + + describe('cancel', () => { + it('closes the publish modal when user clicks on cancel', async () => { + renderElement(); + userEvent.click(screen.getByRole('button', { name: 'Publish' })); + + expect( + screen.getByText('Publish output for the whole hub?'), + ).toBeVisible(); + + userEvent.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'Cancel', + }), + ); + + await waitFor(() => { + expect( + screen.queryByText('Publish output for the whole hub?'), + ).not.toBeInTheDocument(); + }); + }); + + it('closes the version modal when user clicks on cancel', async () => { + renderElement({ isEditing: true, createVersion: true }); + + userEvent.click(screen.getByRole('button', { name: 'Publish' })); + + expect( + screen.getByText('Publish new version for the whole hub?'), + ).toBeVisible(); + + userEvent.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'Cancel', + }), + ); + + await waitFor(() => { + expect( + screen.queryByText('Publish new version for the whole hub?'), + ).not.toBeInTheDocument(); + }); + }); + }); + describe('server side errors', () => { + it('closes the publish modal when user clicks on save and there are server side errors', async () => { + shareOutput.mockRejectedValueOnce(new Error('something went wrong')); + renderElement(); + + userEvent.click(screen.getByRole('button', { name: 'Publish' })); + + expect( + screen.getByText('Publish output for the whole hub?'), + ).toBeVisible(); + + userEvent.click(screen.getByRole('button', { name: /Publish output/i })); + + await waitFor(() => { + expect( + screen.queryByText('Publish output for the whole hub?'), + ).not.toBeInTheDocument(); + }); + }); + it('closes the version modal when user clicks on save and there are server side errors', async () => { + shareOutput.mockRejectedValueOnce(new Error('something went wrong')); + renderElement({ isEditing: true, createVersion: true }); + + userEvent.click(screen.getByRole('button', { name: 'Publish' })); + + expect( + screen.getByText('Publish new version for the whole hub?'), + ).toBeVisible(); + + userEvent.click( + screen.getByRole('button', { name: /Publish new version/i }), + ); + + await waitFor(() => { + expect( + screen.queryByText('Publish new version for the whole hub?'), + ).not.toBeInTheDocument(); + }); + }); + }); +}); diff --git a/packages/gp2-components/src/templates/OutputForm.tsx b/packages/gp2-components/src/templates/OutputForm.tsx index 430adb114e..426b9ccb91 100644 --- a/packages/gp2-components/src/templates/OutputForm.tsx +++ b/packages/gp2-components/src/templates/OutputForm.tsx @@ -17,10 +17,8 @@ import { pixels, ResearchOutputRelatedEventsCard, ajvErrors, - ConfirmModal, OutputVersions, } from '@asap-hub/react-components'; -import { useNotificationContext } from '@asap-hub/react-context'; import { gp2 as gp2Routing } from '@asap-hub/routing'; import { isInternalUser, urlExpression } from '@asap-hub/validation'; import { css } from '@emotion/react'; @@ -35,7 +33,8 @@ import OutputRelatedEventsCard from '../organisms/OutputRelatedEventsCard'; import { Form, OutputIdentifier } from '../organisms'; import OutputRelatedResearchCard from '../organisms/OutputRelatedResearchCard'; import { createIdentifierField, getIconForDocumentType } from '../utils'; -import { EntityMappper } from './CreateOutputPage'; +import { ConfirmAndSaveOutput } from '../organisms/ConfirmAndSaveOutput'; +import { GetWrappedOnSave } from '../organisms/Form'; const { rem } = pixels; const { mailToSupport, INVITE_SUPPORT_EMAIL } = mail; @@ -73,18 +72,6 @@ export const getRelatedEvents = ( label, endDate, })); -const getBannerMessage = ( - entityType: 'workingGroup' | 'project', - documentType: gp2Model.OutputDocumentType, - published: boolean, - createVersion: boolean, -) => - `${createVersion ? 'New ' : ''}${EntityMappper[entityType]} ${documentType} ${ - createVersion ? 'version ' : '' - }${published || createVersion ? 'published' : 'saved'} successfully.`; - -const capitalizeFirstLetter = (string: string) => - string.charAt(0).toUpperCase() + string.slice(1); const footerStyles = css({ display: 'flex', @@ -234,9 +221,6 @@ const OutputForm: React.FC = ({ Boolean(type === 'Blog' || documentType === 'GP2 Reports'), ); - const [displayPublishModal, setDisplayPublishModal] = useState(false); - const [displayVersionModal, setDiplayVersionModal] = useState(false); - const [newTitle, setTitle] = useState(title || ''); const [newLink, setLink] = useState(link || ''); const [newType, setType] = useState(type || ''); @@ -307,26 +291,6 @@ const OutputForm: React.FC = ({ const [identifier, setIdentifier] = useState( doi || rrid || accessionNumber || '', ); - const { addNotification, removeNotification, notifications } = - useNotificationContext(); - - const setBannerMessage = ( - message: string, - page: 'output' | 'output-form', - bannerType: 'error' | 'success', - ) => { - if ( - notifications[0] && - notifications[0]?.message !== capitalizeFirstLetter(message) - ) { - removeNotification(notifications[0]); - } - addNotification({ - message: capitalizeFirstLetter(message), - page, - type: bannerType, - }); - }; const outMainEntity = ({ id }: { id: string }) => id !== mainEntityId; const currentPayload: gp2Model.OutputPostRequest = { @@ -423,394 +387,200 @@ const OutputForm: React.FC = ({ {({ isSaving, getWrappedOnSave, onCancel, setRedirectOnSave }) => { const isEditing = link !== undefined; - const save = async (skipConfirmationModal: boolean = false) => { - const displayModalFn = - !isEditing && !skipConfirmationModal - ? () => { - setDisplayPublishModal(true); - } - : createVersion && !skipConfirmationModal - ? () => { - setDiplayVersionModal(true); - } - : null; - - const output = await getWrappedOnSave( - () => shareOutput(currentPayload), - (error) => setBannerMessage(error, 'output-form', 'error'), - displayModalFn, - )(); - - if (output && typeof output.id === 'string') { - const path = gp2Routing - .outputs({}) - .output({ outputId: output.id }).$; - - setBannerMessage( - getBannerMessage(entityType, documentType, !title, createVersion), - 'output', - 'success', - ); - setRedirectOnSave(path); - } - return output; - }; - return ( <> - {displayPublishModal && ( - setDisplayPublishModal(false)} - confirmText="Publish Output" - onSave={async () => { - const skipPublishModal = true; - const result = await save(skipPublishModal); - if (!result) { - setDisplayPublishModal(false); - } - }} - description={ - <> - All{' '} - {entityType === 'workingGroup' - ? 'working group' - : 'project'}{' '} - members listed on this output will be notified and all GP2 - members will be able to access it. If you need to unpublish - this output, please contact{' '} - {{INVITE_SUPPORT_EMAIL}} - . - - } - /> - )} - - {displayVersionModal && ( - setDiplayVersionModal(false)} - confirmText="Publish new version" - onSave={async () => { - const skipPublishModal = true; - const result = await save(skipPublishModal); - if (!result) { - setDiplayVersionModal(false); - } - }} - description={ - <> - All working group members listed on this output will be - notified and all GP2 members will be able to access it. If - you want to add or edit older versions after this new - version was published, please contact{' '} - {{INVITE_SUPPORT_EMAIL}} - . - - } - /> - )} - -
- {versionList.length > 0 && ( - - )} - - - validationState.valueMissing || - validationState.patternMismatch - ? 'Please enter a title.' - : undefined - } - onChange={(newValue) => { - clearServerValidationError('/title'); - setTitle(newValue); - }} - required - enabled={!isSaving} - /> - { - clearServerValidationError('/link'); - setLink(newValue); - }} - getValidationMessage={(validationState) => - validationState.valueMissing || - validationState.patternMismatch - ? 'Please enter a valid URL, starting with http://' - : undefined - } - customValidationMessage={urlValidationMessage} - value={newLink ?? ''} - enabled={!isSaving} - labelIndicator={} - placeholder="https://example.com" - /> - {documentType === 'Article' && ( - ({ - label: name, - value: name, - }))} - onChange={setType} - /> - )} - {newType === 'Research' && ( - ({ - label: name, - value: name, - }))} - onChange={setSubtype} - /> - )} - - 'Please enter a description'} - required - enabled={!isSaving} - value={newDescription} - info={ - Your Text

^\n\n**Subscript:** ~

Your Text

~\n\n**Hyperlink:** \\[your text](https://example.com)\n\n**New Paragraph:** To create a line break, you will need to press the enter button twice. + + } + setRedirectOnSave={setRedirectOnSave} + documentType={documentType} + title={title} + shareOutput={shareOutput} + createVersion={createVersion} + isEditing={isEditing} + entityType={entityType} + path={(id: string) => + gp2Routing.outputs({}).output({ outputId: id }).$ + } + currentPayload={currentPayload} + > + {({ save }) => ( +
+ {versionList.length > 0 && ( + + )} + + + validationState.valueMissing || + validationState.patternMismatch + ? 'Please enter a title.' + : undefined + } + onChange={(newValue) => { + clearServerValidationError('/title'); + setTitle(newValue); + }} + required + enabled={!isSaving} + /> + { + clearServerValidationError('/link'); + setLink(newValue); + }} + getValidationMessage={(validationState) => + validationState.valueMissing || + validationState.patternMismatch + ? 'Please enter a valid URL, starting with http://' + : undefined + } + customValidationMessage={urlValidationMessage} + value={newLink ?? ''} + enabled={!isSaving} + labelIndicator={} + placeholder="https://example.com" + /> + {documentType === 'Article' && ( + ({ + label: name, + value: name, + }))} + onChange={setType} + /> + )} + {newType === 'Research' && ( + ({ + label: name, + value: name, + }))} + onChange={setSubtype} + /> + )} + + 'Please enter a description'} + required + enabled={!isSaving} + value={newDescription} + info={ + Your Text

^\n\n**Subscript:** ~

Your Text

~\n\n**Hyperlink:** \\[your text](https://example.com)\n\n**New Paragraph:** To create a line break, you will need to press the enter button twice. `} - >
- } - /> - {!DOC_TYPES_GP2_SUPPORTED_NOT_REQUIRED.includes( - documentType, - ) ? ( - - title="Has this output been supported by GP2?" - subtitle="(required)" - options={[ - { - value: 'Yes', - label: 'Yes', - disabled: isSaving, - }, - { - value: 'No', - label: 'No', - disabled: isGP2SupportedAlwaysTrue || isSaving, - }, - { - value: "Don't Know", - label: "Don't Know", - disabled: isGP2SupportedAlwaysTrue || isSaving, - }, - ]} - value={newGp2Supported} - onChange={setGp2Supported} - tooltipText="This option is not available for this document type." - /> - ) : null} - - title="Sharing status" - subtitle="(required)" - options={[ - { - value: 'GP2 Only', - label: 'GP2 Only', - disabled: isAlwaysPublic || isSaving, - }, - { - value: 'Public', - label: 'Public', - disabled: isSaving, - }, - ]} - value={newSharingStatus} - onChange={setSharingStatus} - tooltipText="This option is not available for this document type." - /> - {newSharingStatus === 'Public' ? ( - - setPublishDate(date ? new Date(date) : undefined) - } - enabled={!isSaving} - value={newPublishDate} - max={new Date()} - getValidationMessage={(e) => - getPublishDateValidationMessage(e) - } - /> - ) : null} -
- - - Increase the discoverability of this output by adding - keywords.{' '} - - } - values={newTags.map(({ id, name }) => ({ - label: name, - value: id, - }))} - enabled={!isSaving} - suggestions={tagSuggestions.map(({ id, name }) => ({ - label: name, - value: id, - }))} - onChange={(newValues) => { - setNewTags( - newValues - .slice(0, 10) - .reduce( - (acc, curr) => [ - ...acc, - { id: curr.value, name: curr.label }, - ], - [] as gp2Model.TagDataObject[], - ), - ); - }} - placeholder="Start typing... (E.g. Neurology)" - maxMenuHeight={160} - /> -
- - Ask GP2 to add a new keyword - -
- - {!DOC_TYPES_IDENTIFIER_NOT_REQUIRED.includes(documentType) ? ( - - ) : null} -
- - ({ - label: workingGroup.title, - value: workingGroup.id, - }))} - onChange={(newValues) => { - setWorkingGroups( - newValues - .slice(0, 10) - .reduce( - (acc, curr) => [ - ...acc, - { id: curr.value, title: curr.label }, - ], - [] as gp2Model.OutputOwner[], - ), - ); - }} - values={newWorkingGroups.map((workingGroup, idx) => ({ - label: workingGroup.title, - value: workingGroup.id, - isFixed: idx === 0 && entityType === 'workingGroup', - }))} - noOptionsMessage={({ inputValue }) => - `Sorry, no working groups match ${inputValue}` - } - /> - ({ - label: project.title, - value: project.id, - }))} - onChange={(newValues) => { - setProjects( - newValues - .slice(0, 10) - .reduce( - (acc, curr) => [ - ...acc, - { id: curr.value, title: curr.label }, - ], - [] as gp2Model.OutputOwner[], - ), - ); - }} - values={newProjects.map((project, idx) => ({ - label: project.title, - value: project.id, - isFixed: idx === 0 && entityType === 'project', - }))} - noOptionsMessage={({ inputValue }) => - `Sorry, no projects match ${inputValue}` - } - /> - {!DOC_TYPES_COHORTS_NOT_REQUIRED.includes(documentType) ? ( - <> + > + } + /> + {!DOC_TYPES_GP2_SUPPORTED_NOT_REQUIRED.includes( + documentType, + ) ? ( + + title="Has this output been supported by GP2?" + subtitle="(required)" + options={[ + { + value: 'Yes', + label: 'Yes', + disabled: isSaving, + }, + { + value: 'No', + label: 'No', + disabled: isGP2SupportedAlwaysTrue || isSaving, + }, + { + value: "Don't Know", + label: "Don't Know", + disabled: isGP2SupportedAlwaysTrue || isSaving, + }, + ]} + value={newGp2Supported} + onChange={setGp2Supported} + tooltipText="This option is not available for this document type." + /> + ) : null} + + title="Sharing status" + subtitle="(required)" + options={[ + { + value: 'GP2 Only', + label: 'GP2 Only', + disabled: isAlwaysPublic || isSaving, + }, + { + value: 'Public', + label: 'Public', + disabled: isSaving, + }, + ]} + value={newSharingStatus} + onChange={setSharingStatus} + tooltipText="This option is not available for this document type." + /> + {newSharingStatus === 'Public' ? ( + + setPublishDate(date ? new Date(date) : undefined) + } + enabled={!isSaving} + value={newPublishDate} + max={new Date()} + getValidationMessage={(e) => + getPublishDateValidationMessage(e) + } + /> + ) : null} + + Add other cohorts that contributed to this output. + <> + Increase the discoverability of this output by adding + keywords.{' '} + } - values={newCohorts.map(({ id, name }) => ({ + values={newTags.map(({ id, name }) => ({ label: name, value: id, }))} enabled={!isSaving} - suggestions={cohortSuggestions.map(({ id, name }) => ({ + suggestions={tagSuggestions.map(({ id, name }) => ({ label: name, value: id, }))} onChange={(newValues) => { - setCohorts( + setNewTags( newValues .slice(0, 10) .reduce( @@ -818,70 +588,213 @@ const OutputForm: React.FC = ({ ...acc, { id: curr.value, name: curr.label }, ], - [] as gp2Model.ContributingCohortDataObject[], + [] as gp2Model.TagDataObject[], ), ); }} - placeholder="Start typing..." + placeholder="Start typing... (E.g. Neurology)" maxMenuHeight={160} />
- Don’t see a cohort in this list?{' '} - Contact {INVITE_SUPPORT_EMAIL} + Ask GP2 to add a new keyword
- - ) : null} - - `Sorry, no authors match ${inputValue}` - } - /> -
- - -
-
- -
-
- + + {!DOC_TYPES_IDENTIFIER_NOT_REQUIRED.includes( + documentType, + ) ? ( + + ) : null} + + + ({ + label: workingGroup.title, + value: workingGroup.id, + }), + )} + onChange={(newValues) => { + setWorkingGroups( + newValues + .slice(0, 10) + .reduce( + (acc, curr) => [ + ...acc, + { id: curr.value, title: curr.label }, + ], + [] as gp2Model.OutputOwner[], + ), + ); + }} + values={newWorkingGroups.map((workingGroup, idx) => ({ + label: workingGroup.title, + value: workingGroup.id, + isFixed: idx === 0 && entityType === 'workingGroup', + }))} + noOptionsMessage={({ inputValue }) => + `Sorry, no working groups match ${inputValue}` + } + /> + ({ + label: project.title, + value: project.id, + }))} + onChange={(newValues) => { + setProjects( + newValues + .slice(0, 10) + .reduce( + (acc, curr) => [ + ...acc, + { id: curr.value, title: curr.label }, + ], + [] as gp2Model.OutputOwner[], + ), + ); + }} + values={newProjects.map((project, idx) => ({ + label: project.title, + value: project.id, + isFixed: idx === 0 && entityType === 'project', + }))} + noOptionsMessage={({ inputValue }) => + `Sorry, no projects match ${inputValue}` + } + /> + {!DOC_TYPES_COHORTS_NOT_REQUIRED.includes(documentType) ? ( + <> + + Add other cohorts that contributed to this output. + + } + values={newCohorts.map(({ id, name }) => ({ + label: name, + value: id, + }))} + enabled={!isSaving} + suggestions={cohortSuggestions.map( + ({ id, name }) => ({ + label: name, + value: id, + }), + )} + onChange={(newValues) => { + setCohorts( + newValues + .slice(0, 10) + .reduce( + (acc, curr) => [ + ...acc, + { id: curr.value, name: curr.label }, + ], + [] as gp2Model.ContributingCohortDataObject[], + ), + ); + }} + placeholder="Start typing..." + maxMenuHeight={160} + /> +
+ Don’t see a cohort in this list?{' '} + + Contact {INVITE_SUPPORT_EMAIL} + +
+ + ) : null} + + `Sorry, no authors match ${inputValue}` + } + /> +
+ + +
+
+ +
+
+ +
+
-
-
+ )} +
); }} diff --git a/packages/gp2-components/src/templates/__tests__/OutputForm.test.tsx b/packages/gp2-components/src/templates/__tests__/OutputForm.test.tsx index 2f5592ad87..4e469ce9a8 100644 --- a/packages/gp2-components/src/templates/__tests__/OutputForm.test.tsx +++ b/packages/gp2-components/src/templates/__tests__/OutputForm.test.tsx @@ -109,6 +109,10 @@ describe('OutputForm', () => { { ), }, ); - userEvent.type( - screen.getByRole('textbox', { name: /title/i }), - 'output title', - ); + userEvent.type( screen.getByRole('textbox', { name: /url/i }), 'https://example.com', ); + userEvent.type( - screen.getByRole('textbox', { name: /description/i }), - 'An interesting article', - ); - const gp2Supported = screen.getByRole('group', { - name: /has this output been supported by gp2?/i, - }); - userEvent.click( - within(gp2Supported).getByRole('radio', { name: /yes/i }), - ); - const sharingStatus = screen.getByRole('group', { - name: /sharing status?/i, - }); - userEvent.click( - within(sharingStatus).getByRole('radio', { name: 'GP2 Only' }), + screen.getByRole('textbox', { name: /title/i }), + 'output title', ); + userEvent.click( screen.getByRole('textbox', { name: /identifier type/i }), ); @@ -193,6 +184,7 @@ describe('OutputForm', () => { await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); userEvent.click(screen.getAllByText('Alex White')[1]!); + userEvent.click( screen.getByRole('textbox', { name: /related output/i, @@ -205,8 +197,6 @@ describe('OutputForm', () => { }), ); userEvent.click(await screen.findByText('some related event')); - userEvent.click(screen.getByLabelText(/additional tags/i)); - userEvent.click(screen.getByText('some tag name')); expect( screen.queryByText('Publish output for the whole hub?'), @@ -322,41 +312,7 @@ describe('OutputForm', () => { ), }, ); - userEvent.type( - screen.getByRole('textbox', { name: /title/i }), - 'output title', - ); - userEvent.type( - screen.getByRole('textbox', { name: /url/i }), - 'https://example.com', - ); - userEvent.type( - screen.getByRole('textbox', { name: /description/i }), - 'An interesting article', - ); - const sharingStatus = screen.getByRole('group', { - name: /sharing status?/i, - }); - userEvent.click( - within(sharingStatus).getByRole('radio', { name: 'Public' }), - ); - fireEvent.change( - screen.getByLabelText(/public repository published date/i), - { - target: { value: '2022-03-24' }, - }, - ); - const authors = screen.getByRole('textbox', { name: /Authors/i }); - userEvent.click(authors); - userEvent.click(await screen.findByText(/Chris Reed/i)); - userEvent.click(authors); - userEvent.click(screen.getByText('Chris Blue')); - userEvent.click(authors); - userEvent.type(authors, 'Alex White'); - await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); - userEvent.click(screen.getAllByText('Alex White')[1]!); - userEvent.click(screen.getByRole('textbox', { name: /identifier type/i })); userEvent.click(screen.getByRole('button', { name: 'Publish' })); userEvent.click( screen.getByRole('button', { name: 'Publish new version' }), @@ -764,443 +720,6 @@ describe('OutputForm', () => { }); }); - it('closes the version modal when user clicks on save and there are server side errors', async () => { - const getAuthorSuggestions = jest.fn(); - const history = createMemoryHistory(); - const shareOutput = jest.fn(); - const addNotification = jest.fn(); - getAuthorSuggestions.mockResolvedValue([ - { - author: { - ...gp2Fixtures.createUserResponse(), - displayName: 'Chris Blue', - }, - label: 'Chris Blue', - value: 'u2', - }, - { - author: { - ...gp2Fixtures.createExternalUserResponse(), - displayName: 'Chris Reed', - }, - label: 'Chris Reed (Non CRN)', - value: 'u1', - }, - ]); - - shareOutput.mockRejectedValueOnce(new Error('something went wrong')); - - const publishDate = '2020-03-04'; - const title = 'Output Title'; - const link = 'https://example.com/output'; - const { authors: outputAuthors } = gp2Fixtures.createOutputResponse(); - outputAuthors[0]!.displayName = 'Tony Stark'; - const output = { - ...defaultProps, - ...gp2Fixtures.createOutputResponse(), - publishDate, - title, - link, - authors: outputAuthors, - tags: [{ id: 'tag-1', name: 'Tag' }], - contributingCohorts: [{ id: 'cohort-1', name: 'Cohort' }], - documentType: 'Dataset' as gp2.OutputDocumentType, - }; - - render( - , - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); - userEvent.type( - screen.getByRole('textbox', { name: /title/i }), - 'output title', - ); - userEvent.type( - screen.getByRole('textbox', { name: /url/i }), - 'https://example.com', - ); - userEvent.type( - screen.getByRole('textbox', { name: /description/i }), - 'An interesting article', - ); - const sharingStatus = screen.getByRole('group', { - name: /sharing status?/i, - }); - userEvent.click( - within(sharingStatus).getByRole('radio', { name: 'Public' }), - ); - fireEvent.change( - screen.getByLabelText(/public repository published date/i), - { - target: { value: '2022-03-24' }, - }, - ); - const authors = screen.getByRole('textbox', { name: /Authors/i }); - userEvent.click(authors); - - userEvent.click(await screen.findByText(/Chris Reed/i)); - userEvent.click(authors); - userEvent.click(screen.getByText('Chris Blue')); - userEvent.click(authors); - userEvent.type(authors, 'Alex White'); - await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); - userEvent.click(screen.getAllByText('Alex White')[1]!); - userEvent.click(screen.getByRole('textbox', { name: /identifier type/i })); - userEvent.click(screen.getByRole('button', { name: 'Publish' })); - - expect( - screen.getByText(/Publish new version for the whole hub?/i), - ).toBeVisible(); - expect( - screen.getByRole('button', { name: /Publish new version/i }), - ).toBeVisible(); - - userEvent.click( - screen.getByRole('button', { name: /Publish new version/i }), - ); - - await waitFor(() => { - expect( - screen.queryByText('Publish new version for the whole hub?'), - ).not.toBeInTheDocument(); - }); - }); - - it('closes the publish modal when user clicks on publish and there are server side errors', async () => { - const getAuthorSuggestions = jest.fn(); - const history = createMemoryHistory(); - const shareOutput = jest.fn(); - const addNotification = jest.fn(); - getAuthorSuggestions.mockResolvedValue([ - { - author: { - ...gp2Fixtures.createUserResponse(), - displayName: 'Chris Blue', - }, - label: 'Chris Blue', - value: 'u2', - }, - { - author: { - ...gp2Fixtures.createExternalUserResponse(), - displayName: 'Chris Reed', - }, - label: 'Chris Reed (Non CRN)', - value: 'u1', - }, - ]); - - shareOutput.mockRejectedValueOnce(new Error('something went wrong')); - - render( - , - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); - userEvent.type( - screen.getByRole('textbox', { name: /title/i }), - 'output title', - ); - userEvent.type( - screen.getByRole('textbox', { name: /url/i }), - 'https://example.com', - ); - userEvent.type( - screen.getByRole('textbox', { name: /description/i }), - 'An interesting article', - ); - const sharingStatus = screen.getByRole('group', { - name: /sharing status?/i, - }); - userEvent.click( - within(sharingStatus).getByRole('radio', { name: 'Public' }), - ); - fireEvent.change( - screen.getByLabelText(/public repository published date/i), - { - target: { value: '2022-03-24' }, - }, - ); - const authors = screen.getByRole('textbox', { name: /Authors/i }); - userEvent.click(authors); - - userEvent.click(await screen.findByText(/Chris Reed/i)); - userEvent.click(authors); - userEvent.click(screen.getByText('Chris Blue')); - userEvent.click(authors); - userEvent.type(authors, 'Alex White'); - await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); - userEvent.click(screen.getAllByText('Alex White')[1]!); - userEvent.click(screen.getByRole('textbox', { name: /identifier type/i })); - userEvent.click(screen.getByText('None')); - userEvent.click(screen.getByRole('button', { name: 'Publish' })); - - expect(screen.getByText('Publish output for the whole hub?')).toBeVisible(); - - userEvent.click( - within(screen.getByRole('dialog')).getByRole('button', { - name: 'Publish Output', - }), - ); - - await waitFor(() => { - expect( - screen.queryByText('Publish output for the whole hub?'), - ).not.toBeInTheDocument(); - }); - }); - - it('closes the publish modal when user clicks on cancel', async () => { - const getAuthorSuggestions = jest.fn(); - const history = createMemoryHistory(); - const shareOutput = jest.fn(); - const addNotification = jest.fn(); - getAuthorSuggestions.mockResolvedValue([ - { - author: { - ...gp2Fixtures.createUserResponse(), - displayName: 'Chris Blue', - }, - label: 'Chris Blue', - value: 'u2', - }, - { - author: { - ...gp2Fixtures.createExternalUserResponse(), - displayName: 'Chris Reed', - }, - label: 'Chris Reed (Non CRN)', - value: 'u1', - }, - ]); - shareOutput.mockResolvedValueOnce(gp2Fixtures.createOutputResponse()); - render( - , - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); - userEvent.type( - screen.getByRole('textbox', { name: /title/i }), - 'output title', - ); - userEvent.type( - screen.getByRole('textbox', { name: /url/i }), - 'https://example.com', - ); - userEvent.type( - screen.getByRole('textbox', { name: /description/i }), - 'An interesting article', - ); - const sharingStatus = screen.getByRole('group', { - name: /sharing status?/i, - }); - userEvent.click( - within(sharingStatus).getByRole('radio', { name: 'Public' }), - ); - fireEvent.change( - screen.getByLabelText(/public repository published date/i), - { - target: { value: '2022-03-24' }, - }, - ); - const authors = screen.getByRole('textbox', { name: /Authors/i }); - userEvent.click(authors); - - userEvent.click(await screen.findByText(/Chris Reed/i)); - userEvent.click(authors); - userEvent.click(screen.getByText('Chris Blue')); - userEvent.click(authors); - userEvent.type(authors, 'Alex White'); - await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); - userEvent.click(screen.getAllByText('Alex White')[1]!); - userEvent.click(screen.getByRole('textbox', { name: /identifier type/i })); - userEvent.click(screen.getByText('None')); - userEvent.click(screen.getByRole('button', { name: 'Publish' })); - - expect(screen.getByText('Publish output for the whole hub?')).toBeVisible(); - - userEvent.click( - within(screen.getByRole('dialog')).getByRole('button', { - name: 'Cancel', - }), - ); - - await waitFor(() => { - expect( - screen.queryByText('Publish output for the whole hub?'), - ).not.toBeInTheDocument(); - }); - }); - - it('closes the version modal when user clicks on cancel', async () => { - const getAuthorSuggestions = jest.fn(); - const history = createMemoryHistory(); - const shareOutput = jest.fn(); - const addNotification = jest.fn(); - getAuthorSuggestions.mockResolvedValue([ - { - author: { - ...gp2Fixtures.createUserResponse(), - displayName: 'Chris Blue', - }, - label: 'Chris Blue', - value: 'u2', - }, - { - author: { - ...gp2Fixtures.createExternalUserResponse(), - displayName: 'Chris Reed', - }, - label: 'Chris Reed (Non CRN)', - value: 'u1', - }, - ]); - shareOutput.mockResolvedValueOnce(gp2Fixtures.createOutputResponse()); - - const publishDate = '2020-03-04'; - const title = 'Output Title'; - const link = 'https://example.com/output'; - const { authors: outputAuthors } = gp2Fixtures.createOutputResponse(); - outputAuthors[0]!.displayName = 'Tony Stark'; - const output = { - ...defaultProps, - ...gp2Fixtures.createOutputResponse(), - publishDate, - title, - link, - authors: outputAuthors, - tags: [{ id: 'tag-1', name: 'Tag' }], - contributingCohorts: [{ id: 'cohort-1', name: 'Cohort' }], - documentType: 'Dataset' as gp2.OutputDocumentType, - }; - - render( - , - { - wrapper: ({ children }) => ( - - {children} - - ), - }, - ); - userEvent.type( - screen.getByRole('textbox', { name: /title/i }), - 'output title', - ); - userEvent.type( - screen.getByRole('textbox', { name: /url/i }), - 'https://example.com', - ); - userEvent.type( - screen.getByRole('textbox', { name: /description/i }), - 'An interesting article', - ); - const sharingStatus = screen.getByRole('group', { - name: /sharing status?/i, - }); - userEvent.click( - within(sharingStatus).getByRole('radio', { name: 'Public' }), - ); - fireEvent.change( - screen.getByLabelText(/public repository published date/i), - { - target: { value: '2022-03-24' }, - }, - ); - const authors = screen.getByRole('textbox', { name: /Authors/i }); - userEvent.click(authors); - - userEvent.click(await screen.findByText(/Chris Reed/i)); - userEvent.click(authors); - userEvent.click(screen.getByText('Chris Blue')); - userEvent.click(authors); - userEvent.type(authors, 'Alex White'); - await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); - userEvent.click(screen.getAllByText('Alex White')[1]!); - userEvent.click(screen.getByRole('textbox', { name: /identifier type/i })); - userEvent.click(screen.getByRole('button', { name: 'Publish' })); - - expect( - screen.getByText(/Publish new version for the whole hub?/i), - ).toBeVisible(); - expect( - screen.getByRole('button', { name: /Publish new version/i }), - ).toBeVisible(); - - userEvent.click( - within(screen.getByRole('dialog')).getByRole('button', { - name: 'Cancel', - }), - ); - - await waitFor(() => { - expect( - screen.queryByText('Publish new version for the whole hub?'), - ).not.toBeInTheDocument(); - }); - }); - describe('article', () => { it('renders type', () => { render(, { @@ -1387,21 +906,16 @@ describe('OutputForm', () => { ); test("is set to 'Yes' and make the gp2 supported disabled when type Blog is selected", () => { - render(, { - wrapper: StaticRouter, - }); + render( + , + { + wrapper: StaticRouter, + }, + ); const gp2Supported = screen.getByRole('group', { name: /has this output been supported by gp2?/i, }); - expect( - within(gp2Supported).getByRole('radio', { name: "Don't Know" }), - ).toBeChecked(); - - const input = screen.getByRole('textbox', { name: /^type/i }); - userEvent.click(input); - userEvent.click(screen.getByText('Blog')); - fireEvent.focusOut(input); expect( within(gp2Supported).getByRole('radio', { name: 'Yes' }),