diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 3e6aff553af19936e6e943f7aabd9c70ceceb79c..423059a5c4eb5ac5d6f3a194138524e8124024fc 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -12,8 +12,8 @@ import { } from 'xpub-selectors' import { get as apiGet } from 'pubsweet-client/src/helpers/api' import { compose, lifecycle, withHandlers, withState } from 'recompose' -import { reviewerDecision } from 'pubsweet-components-faraday/src/redux/reviewers' import { getSignedUrl } from 'pubsweet-components-faraday/src/redux/files' +import { reviewerDecision } from 'pubsweet-components-faraday/src/redux/reviewers' import { getHandlingEditors, selectHandlingEditors, @@ -76,6 +76,8 @@ export default compose( const isEic = get(currentUser, 'editorInChief') const isHe = get(currentUser, 'handlingEditor') switch (type) { + case 'isHE': + return isHe case 'staff': return isAdmin || isEic || isHe case 'adminEiC': diff --git a/packages/component-manuscript/src/components/SideBarActions.js b/packages/component-manuscript/src/components/SideBarActions.js index e31d92a43e2cb4ac057523a11b2afc4fbc9277e8..ba0da4b513a84efbc2e01a4f432cfb050d5719e5 100644 --- a/packages/component-manuscript/src/components/SideBarActions.js +++ b/packages/component-manuscript/src/components/SideBarActions.js @@ -1,13 +1,21 @@ import React from 'react' import { th, Icon } from '@pubsweet/ui' import styled from 'styled-components' - import ZipFiles from 'pubsweet-components-faraday/src/components/Files/ZipFiles' +import { Recommendation } from 'pubsweet-components-faraday/src/components/MakeRecommendation' + import { MakeDecision } from './' const SideBarActions = ({ project, version, currentUserIs }) => ( <Root> - {currentUserIs('adminEiC') ? <MakeDecision /> : <div />} + {currentUserIs('adminEiC') && <MakeDecision />} + {currentUserIs('isHE') && ( + <Recommendation + collectionId={project.id} + fragmentId={version.id} + modalKey={`recommend-${version.id}`} + /> + )} <ZipFiles archiveName={`ID-${project.customId}`} collectionId={project.id} diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index e2456344997492259fe08b99369f8d5f30f94ace..200ce0f6c810cd1bc6b2fa7da2bb874a2f91bb78 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -14,24 +14,27 @@ import AuthorsWithTooltip from 'pubsweet-component-manuscript/src/molecules/Auth import ZipFiles from '../Files/ZipFiles' import { InviteReviewers } from '../Reviewers/' +import { currentUserIs } from '../../redux/users' import { selectInvitation } from '../../redux/reviewers' -import { parseVersion, parseJournalIssue, mapStatusToLabel } from './../utils' import { ReviewerDecision, HandlingEditorSection } from './' +import { parseVersion, parseJournalIssue, mapStatusToLabel } from './../utils' import { ReviewerBreakdown } from '../Invitations' +import { Recommendation } from '../MakeRecommendation' const DashboardCard = ({ - deleteProject, + isHE, + theme, + journal, history, project, version, - showAbstractModal, - journal, - showConfirmationModal, - theme, + invitation, currentUser, + deleteProject, + showAbstractModal, canInviteReviewers, - invitation, + showConfirmationModal, ...rest }) => { const { submitted, title, type } = parseVersion(version) @@ -57,6 +60,13 @@ const DashboardCard = ({ /> </LeftDetails> <RightDetails flex={2}> + {isHE && ( + <Recommendation + collectionId={project.id} + fragmentId={version.id} + modalKey={`recommend-${version.id}`} + /> + )} <ZipFiles archiveName={`ID-${project.customId}`} collectionId={project.id} @@ -159,6 +169,7 @@ export default compose( modalComponent: ConfirmationModal, }), connect((state, { project }) => ({ + isHE: currentUserIs(state, 'handlingEditor'), invitation: selectInvitation(state, project.id), })), withHandlers({ diff --git a/packages/components-faraday/src/components/MakeRecommendation/FormItems.js b/packages/components-faraday/src/components/MakeRecommendation/FormItems.js new file mode 100644 index 0000000000000000000000000000000000000000..596aa004273d0d70aabb78efe8505c1c958de1de --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/FormItems.js @@ -0,0 +1,80 @@ +import { th } from '@pubsweet/ui' +import styled from 'styled-components' + +export const RootContainer = styled.div` + background-color: ${th('backgroundColorReverse')}; + border: ${({ bordered }) => (bordered ? th('borderDefault') : 'none')}; + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: 550px; + min-width: 350px; + padding: calc(${th('subGridUnit')} * 2) calc(${th('subGridUnit')} * 4); +` + +export const Title = styled.div` + color: ${th('colorPrimary')}; + font-family: ${th('fontHeading')}; + font-size: ${th('fontSizeHeading5')}; + font-weight: bold; + margin: 10px auto; + text-align: center; +` +export const Subtitle = styled.div` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBase')}; + font-weight: normal; + margin: 10px auto; + text-align: center; +` + +export const Email = styled.div` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBase')}; + font-weight: normal; + margin: 10px auto; + text-align: center; +` + +export const FormContainer = styled.form`` + +export const Row = styled.div` + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-evenly; + margin: calc(${th('subGridUnit')} * 2) 0; +` + +export const RowItem = styled.div` + display: flex; + flex: 1; + flex-direction: ${({ vertical }) => (vertical ? 'column' : 'row')}; + justify-content: ${({ centered }) => (centered ? 'center' : 'initial')}; +` + +export const Label = styled.div` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; + text-transform: uppercase; +` +export const Err = styled.span` + color: ${th('colorError')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBase')}; + margin-top: calc(${th('gridUnit')}*-1); + text-align: center; +` + +export const Textarea = styled.textarea` + width: 100%; + padding: calc(${th('subGridUnit')}*2); + font-size: ${th('fontSizeBaseSmall')}; + font-family: ${th('fontWriting')}; + border-color: ${({ hasError }) => + hasError ? th('colorError') : th('colorPrimary')}; + transition: all 300ms linear; + &:read-only { + background-color: ${th('colorBackgroundHue')}; + } +` diff --git a/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js new file mode 100644 index 0000000000000000000000000000000000000000..3519e1a5eb6371d2f5517b49fac44dfa0c3bcd7c --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js @@ -0,0 +1,96 @@ +import React from 'react' +import { get } from 'lodash' +import { Icon } from '@pubsweet/ui' +import { connect } from 'react-redux' +import styled from 'styled-components' +import { getFormValues, reset as resetForm } from 'redux-form' +import { compose, withState, withHandlers } from 'recompose' + +import { StepOne, StepTwo, RootContainer } from './' +import { + selectError, + selectFetching, + createRecommendation, +} from '../../redux/recommendations' + +const RecommendWizard = ({ + step, + decision, + nextStep, + prevStep, + hideModal, + submitForm, + isFetching, + recommendationError, + ...rest +}) => ( + <RootContainer> + <IconButton onClick={hideModal}> + <Icon primary>x</Icon> + </IconButton> + {step === 0 && ( + <StepOne disabled={!decision} onSubmit={nextStep} {...rest} /> + )} + {step === 1 && ( + <StepTwo + decision={decision} + goBack={prevStep} + isFetching={isFetching} + onSubmit={submitForm} + recommendationError={recommendationError} + /> + )} + </RootContainer> +) + +export default compose( + connect( + state => ({ + isFetching: selectFetching(state), + recommendationError: selectError(state), + decision: get(getFormValues('recommendation')(state), 'decision'), + }), + { + createRecommendation, + resetForm, + }, + ), + withState('step', 'changeStep', 0), + withHandlers({ + nextStep: ({ changeStep }) => () => changeStep(s => s + 1), + prevStep: ({ changeStep }) => () => changeStep(s => (s === 0 ? 0 : s - 1)), + submitForm: ({ + showModal, + hideModal, + resetForm, + fragmentId, + collectionId, + createRecommendation, + }) => values => { + const recommendation = { + recommendation: values.decision, + recommendationType: 'editorRecommendation', + } + if (values.message) { + recommendation.comments = Object.values(values.message).map(m => ({ + content: m, + public: true, + })) + } + createRecommendation(collectionId, fragmentId, recommendation).then(r => { + resetForm('recommendation') + showModal({ + title: 'Recommendation sent', + }) + }) + }, + }), +)(RecommendWizard) + +// #region styled components +const IconButton = styled.div` + align-self: flex-end; + cursor: pointer; +` + +// #endregion diff --git a/packages/components-faraday/src/components/MakeRecommendation/Recommendation.js b/packages/components-faraday/src/components/MakeRecommendation/Recommendation.js new file mode 100644 index 0000000000000000000000000000000000000000..004eb9ebe53b03c89fbe5d46359ec70868f5f611 --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/Recommendation.js @@ -0,0 +1,57 @@ +import React from 'react' +import { th } from '@pubsweet/ui' +import styled from 'styled-components' +import { compose, withHandlers } from 'recompose' + +import { + ConfirmationModal, + withModal2, +} from 'pubsweet-component-modal/src/components' +import { RecommendWizard } from './' + +const Recommendation = ({ showFirstStep }) => ( + <Root onClick={showFirstStep}>Make recommendation</Root> +) + +const SHOW_WIZARD = 'SHOW_WIZARD' + +const ModalComponent = ({ type, ...rest }) => { + switch (type) { + case SHOW_WIZARD: + return <RecommendWizard {...rest} /> + default: + return <ConfirmationModal {...rest} /> + } +} + +export default compose( + withModal2(props => ({ + modalComponent: ModalComponent, + })), + withHandlers({ + showFirstStep: ({ collectionId, fragmentId, showModal }) => () => { + showModal({ + type: SHOW_WIZARD, + collectionId, + fragmentId, + }) + }, + }), +)(Recommendation) + +// #region styled components +const Root = styled.div` + align-items: center; + background-color: ${th('colorPrimary')}; + color: ${th('colorTextReverse')}; + cursor: pointer; + display: flex; + font-family: ${th('fontInterface')}; + font-size: ${th('fontSizeBaseSmall')}; + height: calc(${th('subGridUnit')} * 5); + justify-content: center; + min-width: 200px; + padding: 0 calc(${th('subGridUnit')} * 2); + text-transform: uppercase; +` +// #endregion diff --git a/packages/components-faraday/src/components/MakeRecommendation/StepOne.js b/packages/components-faraday/src/components/MakeRecommendation/StepOne.js new file mode 100644 index 0000000000000000000000000000000000000000..5ce88ae43199a61e31aa07daa504e2bc1c6fb5dd --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/StepOne.js @@ -0,0 +1,44 @@ +import React from 'react' +import { reduxForm } from 'redux-form' +import { RadioGroup, ValidatedField, Button } from '@pubsweet/ui' + +import { RootContainer, Row, RowItem, Title } from './' + +const radioOptions = [ + { value: 'reject', label: 'Reject' }, + { value: 'publish', label: 'Publish' }, + { value: 'revise', label: 'Request revision' }, +] + +const StepOne = ({ hideModal, disabled, onSubmit }) => ( + <RootContainer> + <Title>Recommendation for Next Phase</Title> + <Row> + <RowItem> + <ValidatedField + component={input => ( + <RadioGroup name="decision" options={radioOptions} {...input} /> + )} + name="decision" + /> + </RowItem> + </Row> + <Row> + <RowItem centered> + <Button onClick={hideModal}>Cancel</Button> + </RowItem> + <RowItem centered> + <Button disabled={disabled} onClick={onSubmit} primary> + Next + </Button> + </RowItem> + </Row> + </RootContainer> +) + +export default reduxForm({ + form: 'recommendation', + destroyOnUnmount: false, + enableReinitialize: false, + forceUnregisterOnUnmount: true, +})(StepOne) diff --git a/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js b/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js new file mode 100644 index 0000000000000000000000000000000000000000..f549c1cd513a337f393d597233ad104624bc0a75 --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js @@ -0,0 +1,77 @@ +import React from 'react' +import { capitalize } from 'lodash' +import { compose } from 'recompose' +import { reduxForm } from 'redux-form' +import { Button, Spinner, ValidatedField } from '@pubsweet/ui' + +import { + Row, + Err, + Label, + Title, + RowItem, + Textarea, + RootContainer, + FormContainer, +} from './' + +const Form = RootContainer.withComponent(FormContainer) + +const StepTwo = ({ + recommendationError, + goBack, + decision, + handleSubmit, + isFetching, +}) => ( + <Form onSubmit={handleSubmit}> + <Title>{`Recommandation to ${capitalize(decision)}`}</Title> + <Row> + <RowItem vertical> + <Label>Message for Editor in Chief (optional)</Label> + <ValidatedField + component={input => <Textarea {...input} />} + name="message.eic" + /> + </RowItem> + </Row> + <Row> + <RowItem vertical> + <Label>Message for Author (optional)</Label> + <ValidatedField + component={input => <Textarea {...input} />} + name="message.author" + /> + </RowItem> + </Row> + {recommendationError && ( + <Row> + <RowItem centered> + <Err>{recommendationError}</Err> + </RowItem> + </Row> + )} + <Row> + <RowItem centered> + <Button onClick={goBack}>Back</Button> + </RowItem> + <RowItem centered> + {isFetching ? ( + <Spinner size={3} /> + ) : ( + <Button primary type="submit"> + Submit + </Button> + )} + </RowItem> + </Row> + </Form> +) + +export default compose( + reduxForm({ + form: 'recommendation', + destroyOnUnmount: false, + forceUnregisterOnUnmount: true, + }), +)(StepTwo) diff --git a/packages/components-faraday/src/components/MakeRecommendation/index.js b/packages/components-faraday/src/components/MakeRecommendation/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a8545992fa0500b61b139077740ab9b9a54e2431 --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/index.js @@ -0,0 +1,5 @@ +export * from './FormItems' +export { default as StepOne } from './StepOne' +export { default as StepTwo } from './StepTwo' +export { default as Recommendation } from './Recommendation' +export { default as RecommendWizard } from './RecommendWizard' diff --git a/packages/components-faraday/src/redux/recommendations.js b/packages/components-faraday/src/redux/recommendations.js index a86bfc4758accc0282602783c5927db65940cd0b..067386a832b90a19b2487ab1d5eaff8678438a17 100644 --- a/packages/components-faraday/src/redux/recommendations.js +++ b/packages/components-faraday/src/redux/recommendations.js @@ -61,6 +61,7 @@ export const createRecommendation = ( const errorMessage = get(JSON.parse(error), 'error') dispatch(recommendationsError(errorMessage)) } + throw err }, ) } diff --git a/packages/components-faraday/src/redux/users.js b/packages/components-faraday/src/redux/users.js new file mode 100644 index 0000000000000000000000000000000000000000..dd9cd57ab2db907fbbeb5bd31a46a90afb859c06 --- /dev/null +++ b/packages/components-faraday/src/redux/users.js @@ -0,0 +1,4 @@ +import { get } from 'lodash' + +export const currentUserIs = (state, role) => + get(state, `currentUser.user.${role}`) diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index fec2ea253b827473fa580f507038727af5c95f9b..526f07f749a39fd5a713c9d8f34a82226ba9e21f 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -92,7 +92,9 @@ module.exports = { Joi.object({ id: Joi.string().required(), userId: Joi.string().required(), - recommendationType: Joi.string().required(), + recommendationType: Joi.string() + .valid(['review', 'editorRecommendation']) + .required(), submittedOn: Joi.date(), createdOn: Joi.date(), updatedOn: Joi.date(),