diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index 04d563d5d8662d3baf93c3d71d5cb01fb3718d0b..6c9ebc9280edf6b774437264c1fce3a5324f7245 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -136,6 +136,17 @@ export const userNotConfirmed = ({ currentUser }) => !currentUserIs({ currentUser }, 'staff') && !get(currentUser, 'user.isConfirmed') +export const pendingReviewerInvitation = (state, fragmentId) => + chain(state) + .get(`fragments.${fragmentId}.invitations`, []) + .find( + inv => + inv.userId === get(state, 'currentUser.user.id', '') && + !inv.hasAnswer && + inv.role === 'reviewer', + ) + .value() + export const currentUserIsReviewer = (state, fragmentId) => { const currentUser = selectCurrentUser(state) const invitations = get(state, `fragments.${fragmentId}.invitations`, []) diff --git a/packages/component-faraday-ui/src/AuthorTagList.js b/packages/component-faraday-ui/src/AuthorTagList.js index ff585788d514a0a504b2a16ec7449601e3635e05..2d8268460f6b195f97862ad37951c025ae6915b1 100644 --- a/packages/component-faraday-ui/src/AuthorTagList.js +++ b/packages/component-faraday-ui/src/AuthorTagList.js @@ -45,7 +45,7 @@ const AuthorTagList = ({ authors = [], affiliationList, separator = `, `, - authorKey = 'email', + authorKey = 'id', withTooltip = false, withAffiliations = false, showAffiliation = false, diff --git a/packages/component-faraday-ui/src/InviteReviewers.js b/packages/component-faraday-ui/src/InviteReviewers.js index d48a219b3ba6d4647be5ccbdbc4ce9bba17d43aa..94ce84bf2e74299f42d21386418e181c0fd527ce 100644 --- a/packages/component-faraday-ui/src/InviteReviewers.js +++ b/packages/component-faraday-ui/src/InviteReviewers.js @@ -14,6 +14,7 @@ import { MultiAction, ItemOverrideAlert, withFetching, + validators, withCountries, } from '../' @@ -36,7 +37,7 @@ const InviteReviewers = ({ countries, handleSubmit, reset }) => ( <ValidatedField component={TextField} name="email" - validate={[required]} + validate={[required, validators.emailValidator]} /> </Item> <Item mr={2} vertical> diff --git a/packages/component-faraday-ui/src/ReviewersTable.js b/packages/component-faraday-ui/src/ReviewersTable.js index e031cddbe5662310219b6f21d8ee3e8b6781271b..74ddafc17049e76ed80fcc5b799f1f9091c1bec9 100644 --- a/packages/component-faraday-ui/src/ReviewersTable.js +++ b/packages/component-faraday-ui/src/ReviewersTable.js @@ -1,14 +1,16 @@ import React, { Fragment } from 'react' -import { get } from 'lodash' import styled from 'styled-components' -import { shouldUpdate } from 'recompose' import { th } from '@pubsweet/ui-toolkit' import { DateParser } from '@pubsweet/ui' +import { get, isEqual, orderBy } from 'lodash' +import { compose, shouldUpdate, withHandlers, withProps } from 'recompose' import { Label, PersonInvitation, Text } from '../' const ReviewersTable = ({ invitations, + getInvitationStatus, + renderAcceptedLabel, onResendReviewerInvite, onRevokeReviewerInvite, }) => @@ -40,7 +42,9 @@ const ReviewersTable = ({ 'person.lastName', )}`}</Text> {invitation.isAccepted && ( - <Text customId ml={1}>{`Reviewer ${index + 1}`}</Text> + <Text customId ml={1}> + {renderAcceptedLabel(index)} + </Text> )} </td> <td> @@ -49,23 +53,19 @@ const ReviewersTable = ({ </DateParser> </td> <td> - {invitation.respondedOn && ( - <Fragment> + <Fragment> + {invitation.respondedOn && ( <DateParser timestamp={invitation.respondedOn}> {timestamp => <Text>{timestamp}</Text>} </DateParser> - <Text ml={1} secondary> - ACCEPTED - </Text> - </Fragment> - )} + )} + <Text ml={invitation.respondedOn ? 1 : 0} secondary> + {getInvitationStatus(invitation)} + </Text> + </Fragment> </td> <td> - {invitation.respondedOn && ( - <DateParser timestamp={invitation.respondedOn}> - {timestamp => <Text>{timestamp}</Text>} - </DateParser> - )} + <div /> </td> <HiddenCell> {!invitation.hasAnswer && ( @@ -82,7 +82,33 @@ const ReviewersTable = ({ </Table> ) -export default shouldUpdate(() => false)(ReviewersTable) +const orderInvitations = i => { + if (!i.hasAnswer) return -1 + if (i.isAccepted) return 0 + return 1 +} + +export default compose( + shouldUpdate( + ({ invitations }, { invitations: nextInvitations }) => + !isEqual(invitations, nextInvitations), + ), + withProps(({ invitations = [] }) => ({ + invitations: orderBy(invitations, orderInvitations), + })), + withProps(({ invitations = [] }) => ({ + firstAccepted: invitations.findIndex(i => i.hasAnswer && i.isAccepted), + })), + withHandlers({ + renderAcceptedLabel: ({ firstAccepted, invitations }) => index => + `Reviewer ${index - firstAccepted + 1}`, + getInvitationStatus: () => ({ hasAnswer, isAccepted }) => { + if (!hasAnswer) return 'PENDING' + if (isAccepted) return 'ACCEPTED' + return 'DECLINED' + }, + }), +)(ReviewersTable) // #region styles const Table = styled.table` diff --git a/packages/component-faraday-ui/src/WizardAuthors.js b/packages/component-faraday-ui/src/WizardAuthors.js index 7b283d24311f9906193b4a01b902da8f7d05cb87..29d3c90a5a53dcb2d49d3fb97653edd0f3abebaf 100644 --- a/packages/component-faraday-ui/src/WizardAuthors.js +++ b/packages/component-faraday-ui/src/WizardAuthors.js @@ -14,6 +14,7 @@ import { ActionLink, SortableList, withFetching, + handleError, } from './' const castToBool = author => ({ @@ -178,9 +179,7 @@ export default compose( setFormAuthors(newAuthors) hideModal() }) - .catch(() => { - setModalError('Something went wrong... Please try again.') - }), + .catch(handleError(setModalError)), }), withHandlers({ authorEditorSubmit: ({ diff --git a/packages/component-faraday-ui/src/contextualBoxes/AssignHE.js b/packages/component-faraday-ui/src/contextualBoxes/AssignHE.js index a8319ea3a53ee41ec9febfed5628115d1d186006..38954576b4e6db24ff56c085b747cdca3f919c55 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/AssignHE.js +++ b/packages/component-faraday-ui/src/contextualBoxes/AssignHE.js @@ -123,10 +123,7 @@ export default compose( withHandlers({ inviteHandlingEditor: ({ inviteHandlingEditor }) => ({ email = '', - }) => props => - inviteHandlingEditor(email, props).catch(() => { - props.setModalError('Oops! Something went wrong.') - }), + }) => props => inviteHandlingEditor(email, props), }), setDisplayName('AssignHandlingEditor'), )(AssignHE) diff --git a/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.js b/packages/component-faraday-ui/src/manuscriptDetails/ResponseToInvitation.js similarity index 58% rename from packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.js rename to packages/component-faraday-ui/src/manuscriptDetails/ResponseToInvitation.js index 53f4ca16822becf9cc9eeb0927ad6021ec45a9aa..b0f85ce73dad5312410b2161fadd3469497f691e 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/HandlingEditorAnswer.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/ResponseToInvitation.js @@ -1,9 +1,10 @@ import React from 'react' import { reduxForm } from 'redux-form' -import { get, has, capitalize } from 'lodash' import { required } from 'xpub-validators' -import { compose, withHandlers, withProps } from 'recompose' +import { get, has, capitalize } from 'lodash' +import { compose, withProps } from 'recompose' import { Button, RadioGroup, ValidatedField } from '@pubsweet/ui' +import { withModal } from 'pubsweet-component-modal/src/components' import { Row, @@ -11,7 +12,7 @@ import { Text, Label, Textarea, - OpenModal, + MultiAction, ContextualBox, RowOverrideAlert, withFetching, @@ -22,28 +23,28 @@ const options = [ { label: 'Decline', value: 'decline' }, ] -const HandlingEditorAnswer = ({ +const ResponseToInvitation = ({ + label, + title, toggle, expanded, - decision, isFetching, handleSubmit, onSubmitForm, shouldShowComments, + buttonLabel = 'RESPOND TO INVITATION', }) => ( <ContextualBox expanded={expanded} highlight - label="Respond to Editorial Invitation" + label={title} mb={2} scrollIntoView toggle={toggle} > <RowOverrideAlert justify="flex-start" ml={1} mt={1}> <Item vertical> - <Label required> - Do you agree to be the handling editor for this manuscript? - </Label> + <Label required>{label}</Label> <ValidatedField component={input => ( <RadioGroup inline name="decision" options={options} {...input} /> @@ -69,44 +70,34 @@ const HandlingEditorAnswer = ({ )} <Row justify="flex-end" mb={1} pr={1}> - <OpenModal - cancelText="Close" - confirmText={decision} - isFetching={isFetching} - onConfirm={modalProps => handleSubmit()(modalProps)} - title={`${decision} this invitation?`} - > - {showModal => ( - <Button onClick={onSubmitForm(showModal)} primary size="medium"> - RESPOND TO INVITATION - </Button> - )} - </OpenModal> + <Button onClick={handleSubmit} primary size="medium"> + {buttonLabel} + </Button> </Row> </ContextualBox> ) export default compose( withFetching, - withProps(({ formValues }) => ({ + withModal(({ isFetching, modalKey }) => ({ + modalKey, + isFetching, + modalComponent: MultiAction, + })), + withProps(({ formValues = {}, commentsOn }) => ({ disabled: !has(formValues, 'decision'), - decision: capitalize(get(formValues, 'decision')), - shouldShowComments: get(formValues, 'decision', 'agree') === 'decline', + shouldShowComments: get(formValues, 'decision', 'agree') === commentsOn, })), reduxForm({ - form: 'he-answer-invitation', + form: 'answer-invitation', destroyOnUnmount: false, - onSubmit: (values, dispatch, { onResponse, setFetching }) => modalProps => { - onResponse(values, { ...modalProps, setFetching }) - }, - }), - withHandlers({ - onSubmitForm: ({ disabled, handleSubmit }) => showModal => () => { - if (!disabled) { - showModal() - } else { - handleSubmit() - } + onSubmit: (values, dispatch, { showModal, onResponse, setFetching }) => { + showModal({ + title: `${capitalize(values.decision)} this invitation?`, + onConfirm: modalProps => { + onResponse(values, { ...modalProps, setFetching }) + }, + }) }, }), -)(HandlingEditorAnswer) +)(ResponseToInvitation) diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ResponseToInvitation.md b/packages/component-faraday-ui/src/manuscriptDetails/ResponseToInvitation.md new file mode 100644 index 0000000000000000000000000000000000000000..e7f3a21c9d5f65814365bd1e87054d925a0fb775 --- /dev/null +++ b/packages/component-faraday-ui/src/manuscriptDetails/ResponseToInvitation.md @@ -0,0 +1,42 @@ +A Handling Editor response to an invitation. + +```js +const formValues = { + decision: 'accept', +} +;<RemoteOpener> + {({ toggle, expanded }) => ( + <ResponseToInvitation + commentsOn="decline" + expanded={expanded} + label="Do you agree to be the handling editor for this manuscript?" + formValues={formValues} + onResponse={(values, { setFetching }) => { + console.log('on response: ', values) + setFetching(true) + }} + title="Respond to Editorial Invitation" + toggle={toggle} + /> + )} +</RemoteOpener> +``` + +A Reviewer response to an invitation. + +```js +<RemoteOpener> + {({ toggle, expanded }) => ( + <ResponseToInvitation + expanded={expanded} + label="Do you agree to review this manuscript?" + onResponse={(values, { setFetching }) => { + console.log('on response: ', values) + setFetching(true) + }} + title="Respond to Invitation to Review" + toggle={toggle} + /> + )} +</RemoteOpener> +``` diff --git a/packages/component-faraday-ui/src/manuscriptDetails/index.js b/packages/component-faraday-ui/src/manuscriptDetails/index.js index f19644c15e01a55ec224789fa52896398f7a4afd..c32284d507c22c2d5d4f105916caefc456ef3ea4 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/index.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/index.js @@ -1,4 +1,3 @@ -export { default as HandlingEditorAnswer } from './HandlingEditorAnswer' export { default as ManuscriptDetailsTop } from './ManuscriptDetailsTop' export { default as ManuscriptVersion } from './ManuscriptVersion' export { default as ManuscriptHeader } from './ManuscriptHeader' @@ -7,3 +6,4 @@ export { default as ManuscriptFileList } from './ManuscriptFileList' export { default as ManuscriptFileSection } from './ManuscriptFileSection' export { default as ManuscriptAssignHE } from './ManuscriptAssignHE' export { default as ManuscriptEicDecision } from './ManuscriptEicDecision' +export { default as ResponseToInvitation } from './ResponseToInvitation' diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index d5dc9fe1283200362165b066eb17153234ec756b..e33c34c50a599b8b990deb66f3b006de864023c7 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -7,9 +7,9 @@ import { ManuscriptHeader, ManuscriptAssignHE, ManuscriptMetadata, - HandlingEditorAnswer, ManuscriptDetailsTop, ManuscriptEicDecision, + ResponseToInvitation, paddingHelper, } from 'pubsweet-component-faraday-ui' @@ -50,6 +50,9 @@ const ManuscriptLayout = ({ invitationsWithReviewers, onResendReviewerInvite, onRevokeReviewerInvite, + toggleReviewerResponse, + reviewerResponseExpanded, + onReviewerResponse, }) => ( <Root pb={1}> {!isEmpty(collection) && !isEmpty(fragment) ? ( @@ -82,14 +85,27 @@ const ManuscriptLayout = ({ /> {get(currentUser, 'isInvitedHE', false) && ( - <HandlingEditorAnswer + <ResponseToInvitation + commentsOn="decline" expanded={heResponseExpanded} - formValues={formValues.heInvitation} + formValues={formValues.responseToInvitation} + label="Do you agree to be the handling editor for this manuscript?" onResponse={onHEResponse} + title="Respond to Editorial Invitation" toggle={toggleHEResponse} /> )} + {get(currentUser, 'isInvitedToReview', false) && ( + <ResponseToInvitation + expanded={reviewerResponseExpanded} + label="Do you agree to review this manuscript?" + onResponse={onReviewerResponse} + title="Respond to Invitation to Review" + toggle={toggleReviewerResponse} + /> + )} + <ManuscriptAssignHE assignHE={assignHE} currentUser={currentUser} diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index d75af1720b9a8248c4fef8058861e03da2594c5b..ab2934aa03f294caf6ec2818fb100d527a6d0b30 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -3,7 +3,6 @@ import { actions } from 'pubsweet-client' import { ConnectPage } from 'xpub-connect' import { withJournal } from 'xpub-journal' import { getFormValues } from 'redux-form' -import { replace } from 'react-router-redux' import { withRouter } from 'react-router-dom' import { head, get, isEmpty, isUndefined } from 'lodash' import { @@ -43,6 +42,7 @@ import { currentUserIsReviewer, canMakeRecommendation, parseCollectionDetails, + pendingReviewerInvitation, canOverrideTechnicalChecks, getInvitationsWithReviewersForFragment, } from 'pubsweet-component-faraday-selectors' @@ -85,16 +85,18 @@ export default compose( selectCollection(state, match.params.project), ), pendingHEInvitation: pendingHEInvitation(state, match.params.project), + pendingReviewerInvitation: pendingReviewerInvitation( + state, + match.params.version, + ), editorialRecommendations: selectEditorialRecommendations( state, match.params.version, ), }), { - replace, getSignedUrl, clearCustomError, - reviewerDecision, assignHandlingEditor, createRecommendation, revokeHandlingEditor, @@ -107,15 +109,23 @@ export default compose( connect( ( state, - { pendingHEInvitation, currentUser, match, collection, fragment }, + { + match, + fragment, + collection, + currentUser, + pendingHEInvitation, + pendingReviewerInvitation, + }, ) => ({ currentUser: { ...currentUser, token: getUserToken(state), - isEIC: currentUserIs(state, 'adminEiC'), isHE: currentUserIs(state, 'isHE'), + isEIC: currentUserIs(state, 'adminEiC'), isReviewer: currentUserIsReviewer(state), isInvitedHE: !isUndefined(pendingHEInvitation), + isInvitedToReview: !isUndefined(pendingReviewerInvitation), permissions: { canAssignHE: canAssignHE(state, match.params.project), canInviteReviewers: canInviteReviewers(state, collection), @@ -136,7 +146,7 @@ export default compose( }, formValues: { eicDecision: getFormValues('eic-decision')(state), - heInvitation: getFormValues('he-answer-invitation')(state), + responseToInvitation: getFormValues('answer-invitation')(state), }, invitationsWithReviewers: getInvitationsWithReviewersForFragment( state, @@ -244,9 +254,38 @@ export default compose( history.replace('/') } }) - .catch(() => { + .catch(err => { setFetching(false) - setModalError('Something went wrong...') + handleError(setModalError)(err) + }) + }, + onReviewerResponse: ({ + history, + fragment, + collection, + fetchUpdatedCollection, + pendingReviewerInvitation, + }) => (values, { hideModal, setModalError, setFetching }) => { + const isAccepted = get(values, 'decision', 'decline') === 'accept' + setFetching(true) + reviewerDecision({ + agree: isAccepted, + fragmentId: fragment.id, + collectionId: collection.id, + invitationId: pendingReviewerInvitation.id, + }) + .then(() => { + setFetching(false) + hideModal() + if (isAccepted) { + fetchUpdatedCollection() + } else { + history.replace('/') + } + }) + .catch(err => { + setFetching(false) + handleError(setModalError)(err) }) }, onInviteReviewer: ({ collection, fragment, fetchUpdatedCollection }) => ( @@ -264,9 +303,9 @@ export default compose( hideModal() fetchUpdatedCollection() }) - .catch(() => { + .catch(err => { setFetching(false) - setModalError('Something went wrong...') + handleError(setModalError)(err) }) }, onResendReviewerInvite: ({ @@ -288,9 +327,9 @@ export default compose( hideModal() fetchUpdatedCollection() }) - .catch(() => { + .catch(err => { setFetching(false) - setModalError('Something went wrong...') + handleError(setModalError)(err) }) }, onRevokeReviewerInvite: ({ @@ -309,9 +348,9 @@ export default compose( hideModal() fetchUpdatedCollection() }) - .catch(() => { + .catch(err => { setFetching(false) - setModalError('Something went wrong...') + handleError(setModalError)(err) }) }, }), @@ -323,20 +362,21 @@ export default compose( toggleHEResponse: toggle, heResponseExpanded: expanded, })), + fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ + toggleReviewerResponse: toggle, + reviewerResponseExpanded: expanded, + })), lifecycle({ componentDidMount() { const { match, - replace, history, location, - getFragment, - getCollection, - reviewerDecision, setEditorInChief, clearCustomError, hasManuscriptFailure, - currentUser: { isInvitedHE }, + fetchUpdatedCollection, + currentUser: { isInvitedHE, isInvitedToReview }, } = this.props if (hasManuscriptFailure) { history.push('/not-found') @@ -347,13 +387,10 @@ export default compose( const fragmentId = match.params.version const { agree, invitationId } = parseSearchParams(location.search) if (agree === 'true') { - replace(location.pathname) - reviewerDecision(invitationId, collectionId, fragmentId, true) - .then(() => { - getCollection({ id: collectionId }) - getFragment({ id: collectionId }, { id: fragmentId }) - }) - .catch(redirectToError(replace)) + history.replace(location.pathname) + reviewerDecision({ invitationId, collectionId, fragmentId }) + .then(fetchUpdatedCollection) + .catch(redirectToError(history.replace)) } apiGet(`/users?editorInChief=true`).then(res => @@ -363,6 +400,10 @@ export default compose( if (isInvitedHE) { this.props.toggleHEResponse() } + + if (isInvitedToReview) { + this.props.toggleReviewerResponse() + } }, }), withProps(({ fragment }) => ({ diff --git a/packages/component-manuscript/src/redux/editors.js b/packages/component-manuscript/src/redux/editors.js index 794f4266b2b07ebe31d66f3300895fb7c54596c8..d5f7a8e70c2eee905bc2c7d77999362b816bdf80 100644 --- a/packages/component-manuscript/src/redux/editors.js +++ b/packages/component-manuscript/src/redux/editors.js @@ -84,10 +84,10 @@ export const revokeHandlingEditor = ({ } export const handlingEditorDecision = ({ - invitationId, - collectionId, - isAccepted, reason, + isAccepted, + collectionId, + invitationId, }) => update(`/collections/${collectionId}/invitations/${invitationId}`, { isAccepted, diff --git a/packages/component-wizard/src/components/utils.js b/packages/component-wizard/src/components/utils.js index 393554b8f2e3704bf14fe37daa2d87239270ea7d..9b922cfc1ee32ab719b35ab431cebafdac41cceb 100644 --- a/packages/component-wizard/src/components/utils.js +++ b/packages/component-wizard/src/components/utils.js @@ -8,6 +8,8 @@ import { debounce, isBoolean, } from 'lodash' +import { handleError } from 'pubsweet-component-faraday-ui' + import { autosaveRequest, autosaveSuccess, @@ -115,10 +117,9 @@ export const onSubmit = ( project: collectionId, }) }) - .catch(e => { + .catch(err => { hideModal() - dispatch(autosaveFailure(e)) - setModalError('Something went wrong.') + handleError(setModalError)(err) }) }, onCancel: hideModal, diff --git a/packages/components-faraday/src/components/Admin/AdminUsers.js b/packages/components-faraday/src/components/Admin/AdminUsers.js index 3bb86eafc038e5db34aa46068648afed53c56e2c..0fc5130192cda78f6ac5dba1b246dde4d7d1bc86 100644 --- a/packages/components-faraday/src/components/Admin/AdminUsers.js +++ b/packages/components-faraday/src/components/Admin/AdminUsers.js @@ -18,6 +18,7 @@ import { OpenModal, Pagination, ActionLink, + handleError, withFetching, withPagination, } from 'pubsweet-component-faraday-ui' @@ -174,9 +175,9 @@ export default compose( getUsers() hideModal() }) - .catch(() => { + .catch(err => { setFetching(false) - setModalError('Something went wrong...') + handleError(setModalError)(err) }) }, getUserRoles: ({ journal: { roles = {} } }) => user => { diff --git a/packages/components-faraday/src/components/Admin/utils.js b/packages/components-faraday/src/components/Admin/utils.js index ce4787ba3d305626bfe9ad8ffaea0bfc19204f2c..a923d20778f4b6c7f3424cb69c88f7d85fd9fe88 100644 --- a/packages/components-faraday/src/components/Admin/utils.js +++ b/packages/components-faraday/src/components/Admin/utils.js @@ -1,4 +1,5 @@ import { pick, omit, isBoolean, replace } from 'lodash' +import { handleError } from 'pubsweet-component-faraday-ui' import { update, create } from 'pubsweet-client/src/helpers/api' const generatePasswordHash = () => @@ -104,9 +105,9 @@ export const onSubmit = ( getUsers() hideModal() }) - .catch(e => { + .catch(err => { setFetching(false) - setModalError('Something went wrong...') + handleError(setModalError)(err) }) } return update(`/users/${values.id}`, parseUpdateUser(values)) @@ -115,8 +116,8 @@ export const onSubmit = ( getUsers() hideModal() }) - .catch(e => { + .catch(err => { setFetching(false) - setModalError('Something went wrong...') + handleError(setModalError)(err) }) } diff --git a/packages/components-faraday/src/components/SignUp/ConfirmAccount.js b/packages/components-faraday/src/components/SignUp/ConfirmAccount.js index fd4c7998dfe0040264aa345717de8b47f09dc307..ddd607d01112e21a80ae3643d65188eab577af76 100644 --- a/packages/components-faraday/src/components/SignUp/ConfirmAccount.js +++ b/packages/components-faraday/src/components/SignUp/ConfirmAccount.js @@ -1,8 +1,9 @@ import React from 'react' +import { get } from 'lodash' import { connect } from 'react-redux' import { Button, H2, Spinner } from '@pubsweet/ui' -import { Row, ShadowedBox, Text } from 'pubsweet-component-faraday-ui' import { compose, lifecycle, withState } from 'recompose' +import { Row, Text, ShadowedBox } from 'pubsweet-component-faraday-ui' import { parseSearchParams } from '../utils' import { confirmUser } from '../../redux/users' @@ -11,7 +12,6 @@ const loading = `Loading...` const confirmTitle = `Welcome to Hindawi!` const confirmSubtitle = `Your account has been successfully confirmed.` const errorTitle = `Something went wrong...` -const errorSubtitle = `Please try again.` const ConfirmAccount = ({ message: { title, subtitle }, history }) => ( <ShadowedBox center mt={5}> @@ -51,11 +51,15 @@ export default compose( subtitle: confirmSubtitle, }) }) - .catch(() => { - // errors are still gobbled up by pubsweet + .catch(err => { + const subtitle = get( + JSON.parse(err.response), + 'error', + 'Oops! Something went wrong!', + ) setConfirmMessage({ title: errorTitle, - subtitle: errorSubtitle, + subtitle, }) }) } diff --git a/packages/components-faraday/src/components/SignUp/ReviewerDecline.js b/packages/components-faraday/src/components/SignUp/ReviewerDecline.js index 0b8522d14a13737583a2d80f650ec4fa35179dcd..65a3a8ed8f901048e7deb715b318115c42395ba1 100644 --- a/packages/components-faraday/src/components/SignUp/ReviewerDecline.js +++ b/packages/components-faraday/src/components/SignUp/ReviewerDecline.js @@ -1,68 +1,52 @@ import React from 'react' -import { connect } from 'react-redux' -import styled from 'styled-components' -import { th } from '@pubsweet/ui-toolkit' +import { get } from 'lodash' import { withJournal } from 'xpub-journal' -import { replace } from 'react-router-redux' import { compose, lifecycle } from 'recompose' +import { H2 } from '@pubsweet/ui' +import { + Row, + Text, + ActionLink, + ShadowedBox, +} from 'pubsweet-component-faraday-ui' import { redirectToError } from '../utils' -import { FormItems } from '../UIComponents' import { reviewerDecline } from '../../redux/reviewers' -const { RootContainer, Title } = FormItems +const ReviewerDecline = ({ journal }) => ( + <ShadowedBox center mt={2} width={60}> + <H2>Thank you for letting us know</H2> -const ReviewerDecline = ({ journal: { metadata: { email } } }) => ( - <RootContainer bordered> - <Title>Thank you for letting us know.</Title> - <div> - <Description> + <Row mt={2}> + <Text align="center"> We hope you will review for Hindawi in the future. If you want any more information, or would like to submit a review for this article, then please contact us at{' '} - <MailLink href={`mailto:${email}`} target="_blank"> - {email} - </MailLink>. - </Description> - </div> - </RootContainer> + <ActionLink to={`mailto:${get(journal, 'metadata.email')}`}> + {get(journal, 'metadata.email')} + </ActionLink>. + </Text> + </Row> + </ShadowedBox> ) export default compose( withJournal, - connect(null, { reviewerDecline, replace }), lifecycle({ componentDidMount() { const { + history, + fragmentId, collectionId, invitationId, invitationToken, - reviewerDecline, - replace, - fragmentId, } = this.props - reviewerDecline( + reviewerDecline({ + fragmentId, invitationId, collectionId, - fragmentId, invitationToken, - ).catch(redirectToError(replace)) + }).catch(redirectToError(history.replace)) }, }), )(ReviewerDecline) - -// #region styled-components -const MailLink = styled.a` - color: ${th('colorPrimary')}; - - &:visited { - color: ${th('colorTextPlaceholder')}; - } -` - -const Description = styled.span` - color: ${th('colorPrimary')}; - font-family: ${th('fontReading')}; - font-size: ${th('fontSizeBaseSmall')}; -` -// #endregion diff --git a/packages/components-faraday/src/components/SignUp/ReviewerInviteDecision.js b/packages/components-faraday/src/components/SignUp/ReviewerInviteDecision.js index c0d72b3e10d5ecaa808b74f1dfe17456a1ebff68..917b0073bed22be7d1955dc933f35dc949ca9c03 100644 --- a/packages/components-faraday/src/components/SignUp/ReviewerInviteDecision.js +++ b/packages/components-faraday/src/components/SignUp/ReviewerInviteDecision.js @@ -1,140 +1,158 @@ import React from 'react' -import { get } from 'lodash' import { connect } from 'react-redux' -import { push, replace } from 'react-router-redux' +import { reduxForm } from 'redux-form' import { required, minChars } from 'xpub-validators' -import { reduxForm, SubmissionError } from 'redux-form' import { compose, withState, lifecycle } from 'recompose' import { loginUser } from 'pubsweet-component-login/actions' -import { Button, ValidatedField, TextField } from '@pubsweet/ui' - -import { redirectToError } from '../utils' -import { FormItems } from '../UIComponents' -import { reviewerDecision, setReviewerPassword } from '../../redux/reviewers' - -const { +import { Button, ValidatedField, H2, TextField, Spinner } from '@pubsweet/ui' +import { Row, - Err, - Title, + Item, + Text, Label, - Email, - RowItem, - Subtitle, - RootContainer, - FormContainer, -} = FormItems + ShadowedBox, + handleError, + withFetching, +} from 'pubsweet-component-faraday-ui' + +import { redirectToError, passwordValidator } from '../utils' +import { reviewerDecision, setReviewerPassword } from '../../redux/reviewers' const agreeText = `You have been invited to review a manuscript on the Hindawi platform. Please set a password and proceed to the manuscript.` const declineText = `You have decline to work on a manuscript.` const PasswordField = input => <TextField {...input} type="password" /> const min8Chars = minChars(8) + const ReviewerInviteDecision = ({ agree, error, + isFetching, handleSubmit, errorMessage, reviewerEmail, + fetchingError, }) => ( - <RootContainer bordered> - <Title>Reviewer Invitation</Title> - <Subtitle>{agree === 'true' ? agreeText : declineText}</Subtitle> - <Email>{reviewerEmail}</Email> - {agree === 'true' && ( - <FormContainer onSubmit={handleSubmit}> - <Row> - <RowItem vertical> - <Label> Password </Label> - <ValidatedField - component={PasswordField} - name="password" - validate={[required, min8Chars]} - /> - </RowItem> - </Row> - {error && ( - <Row> - <RowItem> - <Err>Token expired or Something went wrong.</Err> - </RowItem> - </Row> - )} - <Row> - <Button primary type="submit"> - CONFIRM - </Button> - </Row> - </FormContainer> + <ShadowedBox center mt={2} width={60}> + <H2>Reviewer Invitation</H2> + <Text align="center" secondary> + {reviewerEmail} + </Text> + + <Row mt={2}> + <Text align="center">{agree === 'true' ? agreeText : declineText}</Text> + </Row> + + <Row mt={2}> + <Item vertical> + <Label required>Password</Label> + <ValidatedField + component={PasswordField} + name="password" + validate={[required, min8Chars]} + /> + </Item> + </Row> + + <Row mt={2}> + <Item vertical> + <Label required>Confirm password</Label> + <ValidatedField + component={PasswordField} + name="confirmPassword" + validate={[required]} + /> + </Item> + </Row> + + {fetchingError && ( + <Row mt={2}> + <Text align="center" error> + {fetchingError} + </Text> + </Row> )} - </RootContainer> + + <Row mt={2}> + {isFetching ? ( + <Spinner /> + ) : ( + <Button onClick={handleSubmit} primary size="medium"> + CONFIRM + </Button> + )} + </Row> + </ShadowedBox> ) export default compose( + withFetching, withState('reviewerEmail', 'setEmail', ''), connect(null, { - push, - replace, loginUser, - reviewerDecision, - setReviewerPassword, }), lifecycle({ componentDidMount() { const { agree, email, - replace, + history, setEmail, fragmentId, collectionId, invitationId, - reviewerDecision, } = this.props setEmail(email) if (agree === 'false') { - reviewerDecision(invitationId, collectionId, fragmentId, false).catch( - redirectToError(replace), - ) + reviewerDecision({ + fragmentId, + agree: false, + collectionId, + invitationId, + }).catch(redirectToError(history.replace)) } }, }), reduxForm({ form: 'invite-reviewer', + validate: passwordValidator, onSubmit: ( { password }, dispatch, { - push, email, token, - location, + setError, loginUser, fragmentId, + setFetching, collectionId, invitationId, - setReviewerPassword, }, - ) => + ) => { + setFetching(true) + setError('') setReviewerPassword({ email, token, password, }) .then(() => { + setError('') + setFetching(false) loginUser( - { username: email, password }, + { + username: email, + password, + }, `/projects/${collectionId}/versions/${fragmentId}/details?agree=${true}&invitationId=${invitationId}`, ) }) - .catch(error => { - const err = get(error, 'response') - if (err) { - const errorMessage = get(JSON.parse(err), 'error') - throw new SubmissionError({ - _error: errorMessage || 'Something went wrong', - }) - } - }), + .catch(err => { + setFetching(false) + handleError(setError)(err) + }) + }, }), )(ReviewerInviteDecision) diff --git a/packages/components-faraday/src/components/UIComponents/ErrorPage.js b/packages/components-faraday/src/components/UIComponents/ErrorPage.js index fe9ffab27d2aca61eac794b502ec4b1ed68bfb74..ca1052e09a82b2a4f923a7b688f77bfcac8dd841 100644 --- a/packages/components-faraday/src/components/UIComponents/ErrorPage.js +++ b/packages/components-faraday/src/components/UIComponents/ErrorPage.js @@ -1,35 +1,21 @@ import React from 'react' -import { Button } from '@pubsweet/ui' -import styled from 'styled-components' -import { th } from '@pubsweet/ui-toolkit' +import { Button, H2 } from '@pubsweet/ui' +import { Row, ShadowedBox, Text } from 'pubsweet-component-faraday-ui' const ErrorPage = ({ location: { state }, history }) => ( - <Root> - <Title>{state}</Title> - <Button onClick={() => history.push('/')} primary> - Go to Dashboard - </Button> - </Root> -) - -export default ErrorPage + <ShadowedBox center mt={2} width={60}> + <H2>Error</H2> -// #region styles -const Root = styled.div` - color: ${th('colorText')}; - margin: 0 auto; - text-align: center; - width: 70vw; + <Row mt={2}> + <Text align="center">{state}</Text> + </Row> - a { - color: ${th('colorText')}; - } -` + <Row mt={2}> + <Button onClick={() => history.push('/')} primary> + Go to Dashboard + </Button> + </Row> + </ShadowedBox> +) -const Title = styled.div` - color: ${th('colorPrimary')}; - font-size: ${th('fontSizeHeading5')}; - font-family: ${th('fontHeading')}; - margin: calc(${th('subGridUnit')} * 2) auto; -` -// #endregion +export default ErrorPage diff --git a/packages/components-faraday/src/components/UserProfile/EmailNotifications.js b/packages/components-faraday/src/components/UserProfile/EmailNotifications.js index e69d09cb6e39c81824ee6566a4a84ac932294b0d..aa27b705dab177bc1326660780407e50282a2e70 100644 --- a/packages/components-faraday/src/components/UserProfile/EmailNotifications.js +++ b/packages/components-faraday/src/components/UserProfile/EmailNotifications.js @@ -7,6 +7,7 @@ import { OpenModal, ActionLink, ShadowedBox, + handleError, withFetching, } from 'pubsweet-component-faraday-ui' @@ -54,9 +55,9 @@ export default compose( setFetching(false) hideModal() }) - .catch(() => { + .catch(err => { setFetching(false) - setModalError('Oops! Something went wrong...') + handleError(setModalError)(err) }) }, }), diff --git a/packages/components-faraday/src/components/UserProfile/UserProfilePage.js b/packages/components-faraday/src/components/UserProfile/UserProfilePage.js index f77295673fdd027c32abda4c6328a58f8c0c1c9d..6c507ad66e750a5c49d9faa783230e46d5d75955 100644 --- a/packages/components-faraday/src/components/UserProfile/UserProfilePage.js +++ b/packages/components-faraday/src/components/UserProfile/UserProfilePage.js @@ -11,6 +11,7 @@ import { Text, ActionLink, UserProfile, + handleError, } from 'pubsweet-component-faraday-ui' import { saveUserDetails } from '../utils' @@ -74,9 +75,7 @@ export default compose( setFetching(false) toggleEdit() }) - .catch(() => { - setError('Something went wrong... Please try again.') - }) + .catch(handleError(setError)) }, unlinkOrcid: ({ user, saveUserDetails }) => ({ hideModal, @@ -91,9 +90,9 @@ export default compose( setFetching(false) hideModal() }) - .catch(() => { + .catch(err => { setFetching(false) - setModalError('Something went wrong... Please try again.') + handleError(setModalError)(err) }) }, }), diff --git a/packages/components-faraday/src/index.js b/packages/components-faraday/src/index.js index db4b6cd565c1db6e4afba19f44272d8200786c50..f2e244a4264d4b2de2337305c35fc60ce134aaa7 100644 --- a/packages/components-faraday/src/index.js +++ b/packages/components-faraday/src/index.js @@ -5,7 +5,6 @@ module.exports = { authors: () => require('./redux/authors').default, customError: () => require('./redux/errors').default, files: () => require('./redux/files').default, - reviewers: () => require('./redux/reviewers').default, technicalCheck: () => require('./redux/technicalCheck').default, }, }, diff --git a/packages/components-faraday/src/redux/reviewers.js b/packages/components-faraday/src/redux/reviewers.js index 204911271f2ae06fc53298afd4bbe7912705a493..7542b980da4cdb1aaab21256f0b85bca2cabfb3d 100644 --- a/packages/components-faraday/src/redux/reviewers.js +++ b/packages/components-faraday/src/redux/reviewers.js @@ -1,48 +1,13 @@ -import { get, orderBy } from 'lodash' +import { get } from 'lodash' import { selectCurrentUser } from 'xpub-selectors' import { - get as apiGet, create, remove, update, + get as apiGet, } from 'pubsweet-client/src/helpers/api' -import { orderReviewers } from './utils' - -const GET_REVIEWERS_REQUEST = 'GET_REVIEWERS_REQUEST' -const GET_REVIEWERS_ERROR = 'GET_REVIEWERS_ERROR' -const GET_REVIEWERS_SUCCESS = 'GET_REVIEWERS_SUCCESS' - -export const getReviewersRequest = () => ({ - type: GET_REVIEWERS_REQUEST, -}) - -export const getReviewersError = error => ({ - type: GET_REVIEWERS_ERROR, - error, -}) - -export const getReviewersSuccess = reviewers => ({ - type: GET_REVIEWERS_SUCCESS, - payload: { reviewers }, -}) - -// reviewer invite constants and action creators -const INVITE_REVIEWER_REQUEST = 'INVITE_REVIEWER_REQUEST' -const INVITE_REVIEWER_SUCCESS = 'INVITE_REVIEWER_SUCCESS' -const INVITE_REVIEWER_ERROR = 'INVITE_REVIEWER_ERROR' // reviewer decision constants and action creators -const REVIEWER_DECISION_REQUEST = 'REVIEWER_DECISION_REQUEST' -const REVIEWER_DECISION_ERROR = 'REVIEWER_DECISION_ERROR' -const REVIEWER_DECISION_SUCCESS = 'REVIEWER_DECISION_SUCCESS' - -const reviewerDecisionRequest = () => ({ type: REVIEWER_DECISION_REQUEST }) -const reviewerDecisionError = error => ({ - type: REVIEWER_DECISION_ERROR, - error, -}) -const reviewerDecisionSuccess = () => ({ type: REVIEWER_DECISION_SUCCESS }) - const initialState = { fetching: { decision: false, @@ -88,21 +53,10 @@ export const currentUserIsReviewer = (state, fragmentId) => { ) } -export const getCollectionReviewers = ( - collectionId, - fragmentId, -) => dispatch => { - dispatch(getReviewersRequest()) - return apiGet( +export const getCollectionReviewers = (collectionId, fragmentId) => dispatch => + apiGet( `/collections/${collectionId}/fragments/${fragmentId}/invitations?role=reviewer`, - ).then( - r => dispatch(getReviewersSuccess(orderBy(r, orderReviewers))), - err => { - dispatch(getReviewersError(err)) - throw err - }, ) -} // #endregion export const inviteReviewer = ({ reviewerData, collectionId, fragmentId }) => @@ -112,13 +66,8 @@ export const inviteReviewer = ({ reviewerData, collectionId, fragmentId }) => }) // #region Actions - invitations -export const setReviewerPassword = reviewerBody => dispatch => { - dispatch(reviewerDecisionRequest()) - return create(`/users/reset-password`, reviewerBody).then(r => { - dispatch(reviewerDecisionSuccess()) - return r - }) -} +export const setReviewerPassword = reviewerBody => + create(`/users/reset-password`, reviewerBody) export const revokeReviewer = ({ fragmentId, collectionId, invitationId }) => remove( @@ -127,137 +76,36 @@ export const revokeReviewer = ({ fragmentId, collectionId, invitationId }) => // #endregion // #region Actions - decision -export const reviewerDecision = ( - invitationId, - collectionId, +export const reviewerDecision = ({ fragmentId, agree = true, -) => dispatch => { - dispatch(reviewerDecisionRequest()) - return update( + collectionId, + invitationId, +}) => + update( `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}`, { isAccepted: agree, }, - ).then( - res => { - dispatch(reviewerDecisionSuccess()) - return res - }, - err => { - dispatch(reviewerDecisionError(err.message)) - throw err - }, ) -} -export const reviewerDecline = ( - invitationId, - collectionId, +export const reviewerDecline = ({ fragmentId, + collectionId, + invitationId, invitationToken, -) => dispatch => { - dispatch(reviewerDecisionRequest()) - return update( +}) => + update( `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}/decline`, { invitationToken, }, - ).then( - res => { - dispatch(reviewerDecisionSuccess()) - return res - }, - err => { - dispatch(reviewerDecisionError(err.message)) - throw err - }, ) -} // #endregion // #region Reducer export default (state = initialState, action = {}) => { switch (action.type) { - case GET_REVIEWERS_REQUEST: - return { - ...state, - fetching: { - ...state.fetching, - reviewers: true, - }, - reviewers: [], - } - case GET_REVIEWERS_ERROR: - return { - ...state, - fetching: { - ...state.fetching, - reviewers: false, - }, - error: action.error, - } - case GET_REVIEWERS_SUCCESS: - return { - ...state, - fetching: { - ...state.fetching, - reviewers: false, - }, - reviewers: action.payload.reviewers, - } - case INVITE_REVIEWER_REQUEST: - return { - ...state, - fetching: { - ...state.fetching, - invite: true, - }, - } - case INVITE_REVIEWER_SUCCESS: - return { - ...state, - fetching: { - ...state.fetching, - invite: false, - }, - error: null, - } - case INVITE_REVIEWER_ERROR: - return { - ...state, - fetching: { - ...state.fetching, - invite: false, - }, - error: action.error, - } - case REVIEWER_DECISION_REQUEST: - return { - ...state, - fetching: { - ...state.fetching, - decision: true, - }, - } - case REVIEWER_DECISION_ERROR: - return { - ...state, - fetching: { - ...state.fetching, - decision: false, - }, - error: action.error, - } - case REVIEWER_DECISION_SUCCESS: - return { - ...state, - fetching: { - ...state.fetching, - decision: false, - }, - error: null, - } case CLEAR_ERROR: { return { ...state,