From e8d6908b47973641cfb2d0db3a770786de5d674e Mon Sep 17 00:00:00 2001 From: Alexandru Munteanu <alexandru.munt@gmail.com> Date: Thu, 4 Oct 2018 11:40:51 +0300 Subject: [PATCH] feat(HE-recommendation): add contextual box with HE form --- .../component-faraday-selectors/src/index.js | 22 ++++-- .../component-faraday-ui/src/RemoteOpener.md | 2 +- .../EditorialRecommendation.js | 72 ++++++++++++++----- .../src/contextualBoxes/ReviewerDetails.js | 6 +- .../src/components/ManuscriptLayout.js | 24 ++++--- .../src/components/ManuscriptPage.js | 46 +++++++++++- .../src/redux/recommendations.js | 1 - .../src/redux/recommendations.js | 2 + 8 files changed, 141 insertions(+), 34 deletions(-) diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index e852fc3b0..226cee8a4 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -2,10 +2,12 @@ import { get, has, last, chain } from 'lodash' import { selectCurrentUser } from 'xpub-selectors' export const isHEToManuscript = (state, collectionId) => { - const currentUserId = get(state, 'currentUser.user.id', '') - const collections = get(state, 'collections', []) - const collection = collections.find(c => c.id === collectionId) || {} - return get(collection, 'handlingEditor.id') === currentUserId + const { id = '', isAccepted = false } = chain(state) + .get('collections', []) + .find(c => c.id === collectionId) + .get('handlingEditor.id', '') + .value() + return isAccepted && id === get(state, 'currentUser.user.id') } export const currentUserIs = ({ currentUser: { user } }, role) => { @@ -217,6 +219,18 @@ export const getInvitationsWithReviewersForFragment = (state, fragmentId) => })) .value() +export const canMakeHERecommendation = (state, { collection, statuses }) => { + const { isAccepted, id: heId } = get(collection, 'handlingEditor', {}) + const validHe = isAccepted && get(state, 'currentUser.user.id', '') === heId + const statusImportance = get( + statuses, + `${get(collection, 'status', 'draft')}.importance`, + 1, + ) + + return statusImportance > 1 && statusImportance < 9 && validHe +} + // #region Editorial and reviewer recommendations export const getFragmentRecommendations = (state, fragmentId) => get(state, `fragments.${fragmentId}.recommendations`, []) diff --git a/packages/component-faraday-ui/src/RemoteOpener.md b/packages/component-faraday-ui/src/RemoteOpener.md index 283c505e2..173a86f04 100644 --- a/packages/component-faraday-ui/src/RemoteOpener.md +++ b/packages/component-faraday-ui/src/RemoteOpener.md @@ -2,7 +2,7 @@ Toggle a boolean flag and pass it around in your React components tree. ```js <RemoteOpener> - {(expanded, toggle) => ( + {({ expanded, toggle }) => ( <div> <button onClick={toggle}>Toggle</button> <span>{expanded ? 'Collapse me!' : 'Expand me!'}</span> diff --git a/packages/component-faraday-ui/src/contextualBoxes/EditorialRecommendation.js b/packages/component-faraday-ui/src/contextualBoxes/EditorialRecommendation.js index 679610502..beb1fcb68 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/EditorialRecommendation.js +++ b/packages/component-faraday-ui/src/contextualBoxes/EditorialRecommendation.js @@ -1,5 +1,5 @@ import React from 'react' -import { get } from 'lodash' +import { get, tail } from 'lodash' import { reduxForm } from 'redux-form' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' @@ -22,18 +22,41 @@ import { const options = [ { value: 'publish', label: 'Publish' }, { value: 'reject', label: 'Reject' }, - { value: 'minor-revision', label: 'Request Minor Revision' }, - { value: 'major-revision', label: 'Request Major Revision' }, + { value: 'minor', label: 'Request Minor Revision' }, + { value: 'major', label: 'Request Major Revision' }, ] -const EditorialRecommendation = ({ formValues, handleSubmit }) => ( - <ContextualBox highlight label="Your Editorial Recommendation" startExpanded> +const parseFormValues = ({ recommendation, ...rest }) => { + const comments = Object.entries(rest).map(([key, value]) => ({ + content: value, + public: key === 'public', + files: [], + })) + + return { + comments, + recommendation, + recommendationType: 'editorRecommendation', + } +} + +const EditorialRecommendation = ({ + formValues, + handleSubmit, + hasReviewerReports, +}) => ( + <ContextualBox highlight label="Your Editorial Recommendation" mb={2}> <Root> <Row justify="flex-start"> <ItemOverrideAlert flex={0} vertical> <Label required>Recommendation</Label> <ValidatedField - component={input => <Menu options={options} {...input} />} + component={input => ( + <Menu + options={hasReviewerReports ? options : tail(options)} + {...input} + /> + )} name="recommendation" validate={[required]} /> @@ -47,27 +70,27 @@ const EditorialRecommendation = ({ formValues, handleSubmit }) => ( <Label required>Message for Author</Label> <ValidatedField component={Textarea} - name="message" + name="public" validate={[required]} /> </ItemOverrideAlert> </Row> ) : ( - <Row mt={2}> - <ItemOverrideAlert mr={1} vertical> + <ResponsiveRow mt={2}> + <ResponsiveItem mr={1} vertical> <Label> Message for Author <Text secondary>Optional</Text> </Label> - <ValidatedField component={Textarea} name="author-message" /> - </ItemOverrideAlert> + <ValidatedField component={Textarea} name="public" /> + </ResponsiveItem> - <ItemOverrideAlert ml={1} vertical> + <ResponsiveItem ml={1} vertical> <Label> Message for Editor in Chief <Text secondary>Optional</Text> </Label> - <ValidatedField component={Textarea} name="eic-message" /> - </ItemOverrideAlert> - </Row> + <ValidatedField component={Textarea} name="private" /> + </ResponsiveItem> + </ResponsiveRow> )} <Row justify="flex-end" mt={2}> @@ -100,7 +123,10 @@ export default compose( showModal({ title: `${modalTitle}?`, onConfirm: props => { - onRecommendationSubmit(values, { ...props, setFetching }) + onRecommendationSubmit(parseFormValues(values), { + ...props, + setFetching, + }) }, }) }, @@ -113,4 +139,18 @@ const Root = styled.div` flex-direction: column; padding: ${th('gridUnit')}; ` + +const ResponsiveRow = styled(Row)` + @media (max-width: 800px) { + flex-direction: column; + } +` + +const ResponsiveItem = styled(ItemOverrideAlert)` + @media (max-width: 800px) { + margin-right: 0; + margin-left: 0; + width: 100%; + } +` // #endregion diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js index 153dbfa01..e0b21c0c3 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js @@ -31,11 +31,15 @@ const ReviewerDetails = ({ onInviteReviewer, onResendReviewerInvite, onRevokeReviewerInvite, + toggle, + expanded, }) => ( <ContextualBox + expanded={expanded} label="Reviewer details" rightChildren={<ReviewerBreakdown fitContent fragment={fragment} mr={1} />} - startExpanded + scrollIntoView + toggle={toggle} > <Tabs> {({ selectedTab, changeTab }) => ( diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index 4e2471f1d..d0178984f 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -62,7 +62,12 @@ const ManuscriptLayout = ({ reviewerRecommendationExpanded, shouldReview, submittedOwnRecommendation, + heAccepted, reviewerReports, + onEditorialRecommendation, + reviewerRecommendations, + toggleReviewerDetails, + reviewerDetailsExpanded, }) => ( <Root pb={30}> {!isEmpty(collection) && !isEmpty(fragment) ? ( @@ -88,14 +93,6 @@ const ManuscriptLayout = ({ revokeInvitation={revokeHE} /> - <EditorialRecommendation - formValues={get(formValues, 'editorialRecommendation', {})} - modalKey="heRecommendation" - onRecommendationSubmit={(values, props) => { - props.setFetching(true) - }} - /> - <ManuscriptMetadata currentUser={currentUser} fragment={fragment} @@ -171,9 +168,19 @@ const ManuscriptLayout = ({ /> )} + {get(currentUser, 'permissions.canMakeHERecommendation', false) && ( + <EditorialRecommendation + formValues={get(formValues, 'editorialRecommendation', {})} + hasReviewerReports={reviewerRecommendations.length > 0} + modalKey="heRecommendation" + onRecommendationSubmit={onEditorialRecommendation} + /> + )} + {get(currentUser, 'permissions.canInviteReviewers', false) && ( <ReviewerDetails currentUser={currentUser} + expanded={reviewerDetailsExpanded} fragment={fragment} getSignedUrl={getSignedUrl} invitations={invitationsWithReviewers} @@ -182,6 +189,7 @@ const ManuscriptLayout = ({ onResendReviewerInvite={onResendReviewerInvite} onRevokeReviewerInvite={onRevokeReviewerInvite} reviewerReports={reviewerReports} + toggle={toggleReviewerDetails} /> )} </Fragment> diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index ce588ddfe..6481bd89d 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -36,11 +36,13 @@ import { currentUserIs, canMakeRevision, canMakeDecision, + isHEToManuscript, canEditManuscript, canInviteReviewers, pendingHEInvitation, currentUserIsReviewer, parseCollectionDetails, + canMakeHERecommendation, pendingReviewerInvitation, canOverrideTechnicalChecks, getOwnPendingRecommendation, @@ -126,6 +128,7 @@ export default compose( state, { match, + journal, fragment, collection, currentUser, @@ -139,10 +142,18 @@ export default compose( token: getUserToken(state), isHE: currentUserIs(state, 'isHE'), isEIC: currentUserIs(state, 'adminEiC'), - isReviewer: currentUserIsReviewer(state, get(fragment, 'id', '')), isInvitedHE: !isUndefined(pendingHEInvitation), isInvitedToReview: !isUndefined(pendingReviewerInvitation), + isReviewer: currentUserIsReviewer(state, get(fragment, 'id', '')), + isHEToManuscript: isHEToManuscript(state, { + collectionId: get(collection, 'id', ''), + statuses: get(journal, 'statuses', {}), + }), permissions: { + canMakeHERecommendation: canMakeHERecommendation(state, { + collection, + statuses: get(journal, 'statuses', {}), + }), canAssignHE: canAssignHE(state, match.params.project), canInviteReviewers: canInviteReviewers(state, collection), canMakeRecommendation: !isUndefined(pendingOwnRecommendation), @@ -373,6 +384,27 @@ export default compose( handleError(setModalError)(err) }) }, + onEditorialRecommendation: ({ + fragment, + collection, + fetchUpdatedCollection, + }) => (recommendation, { hideModal, setFetching, setModalError }) => { + setFetching(true) + createRecommendation({ + recommendation, + fragmentId: fragment.id, + collectionId: collection.id, + }) + .then(r => { + setFetching(false) + hideModal() + fetchUpdatedCollection() + }) + .catch(e => { + setFetching(false) + handleError(setModalError)(e) + }) + }, }), fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ toggleAssignHE: toggle, @@ -390,7 +422,11 @@ export default compose( toggleReviewerRecommendations: toggle, reviewerRecommendationExpanded: expanded, })), - withProps(({ currentUser, submittedOwnRecommendation }) => ({ + fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ + toggleReviewerDetails: toggle, + reviewerDetailsExpanded: expanded, + })), + withProps(({ currentUser, collection, submittedOwnRecommendation }) => ({ getSignedUrl, shouldReview: get(currentUser, 'isReviewer', false) && @@ -407,7 +443,7 @@ export default compose( clearCustomError, hasManuscriptFailure, fetchUpdatedCollection, - currentUser: { isInvitedHE, isInvitedToReview }, + currentUser: { isInvitedHE, isInvitedToReview, isHEToManuscript }, } = this.props if (hasManuscriptFailure) { history.push('/not-found') @@ -439,6 +475,10 @@ export default compose( if (shouldReview) { this.props.toggleReviewerRecommendations() } + + if (isHEToManuscript) { + this.props.toggleReviewerDetails() + } }, }), )(ManuscriptLayout) diff --git a/packages/component-manuscript/src/redux/recommendations.js b/packages/component-manuscript/src/redux/recommendations.js index bb55354a5..2f57b4f37 100644 --- a/packages/component-manuscript/src/redux/recommendations.js +++ b/packages/component-manuscript/src/redux/recommendations.js @@ -23,7 +23,6 @@ export const recommendationsFetching = state => // #endregion // #region Actions -// error handling and fetching is handled by the autosave reducer export const createRecommendation = ({ fragmentId, collectionId, diff --git a/packages/components-faraday/src/redux/recommendations.js b/packages/components-faraday/src/redux/recommendations.js index fb3d21fef..6664e1358 100644 --- a/packages/components-faraday/src/redux/recommendations.js +++ b/packages/components-faraday/src/redux/recommendations.js @@ -4,10 +4,12 @@ import { create, update } from 'pubsweet-client/src/helpers/api' // #region Selectors export const selectRecommendations = (state, fragmentId) => get(state, `fragments.${fragmentId}.recommendations`, []) + export const selectEditorialRecommendations = (state, fragmentId) => selectRecommendations(state, fragmentId).filter( r => r.recommendationType === 'editorRecommendation' && r.comments, ) + export const selectReviewRecommendations = (state, fragmentId) => selectRecommendations(state, fragmentId).filter( r => r.recommendationType === 'review', -- GitLab