diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index 46e0a68e5e5a64893e55c6195b460ab49569b838..8ad118184572d279c3317425e1cd4ad2d0f6dda3 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -1,4 +1,4 @@ -import { get, last, chain } from 'lodash' +import { get, has, last, chain } from 'lodash' import { selectCurrentUser } from 'xpub-selectors' export const isHEToManuscript = (state, collectionId) => { @@ -218,18 +218,6 @@ export const getInvitationsWithReviewersForFragment = (state, fragmentId) => .value() // #region Editorial and reviewer recommendations -const canMakeRecommendationStatuses = [ - 'heAssigned', - 'underReview', - 'reviewCompleted', -] -export const canMakeRecommendation = (state, collection, fragment = {}) => { - if (fragment.id !== last(get(collection, 'fragments', []))) return false - const isHE = isHEToManuscript(state, get(collection, 'id', '')) - const status = get(collection, 'status', 'draft') - return isHE && canMakeRecommendationStatuses.includes(status) -} - export const getFragmentRecommendations = (state, fragmentId) => get(state, `fragments.${fragmentId}.recommendations`, []) @@ -238,9 +226,39 @@ export const getFragmentReviewerRecommendations = (state, fragmentId) => r => r.recommendationType === 'review', ) -export const getOwnRecommendation = (state, fragmentId) => +const getOwnRecommendations = (state, fragmentId) => chain(state) .get(`fragments.${fragmentId}.recommendations`, []) - .find(r => r.userId === get(state, 'currentUser.user.id', '')) + .filter(r => r.userId === get(state, 'currentUser.user.id', '')) + .value() + +export const getOwnPendingRecommendation = (state, fragmentId) => + chain(getOwnRecommendations(state, fragmentId)) + .find( + r => + r.userId === get(state, 'currentUser.user.id', '') && + !has(r, 'submittedOn'), + ) .value() + +export const getOwnSubmittedRecommendation = (state, fragmentId) => + chain(getOwnRecommendations(state, fragmentId)) + .find( + r => + r.userId === get(state, 'currentUser.user.id', '') && + has(r, 'submittedOn'), + ) + .value() + +const canMakeRecommendationStatuses = [ + 'heAssigned', + 'underReview', + 'reviewCompleted', +] +export const canMakeRecommendation = (state, collection, fragment = {}) => { + if (fragment.id !== last(get(collection, 'fragments', []))) return false + const isHE = isHEToManuscript(state, get(collection, 'id', '')) + const status = get(collection, 'status', 'draft') + return isHE && canMakeRecommendationStatuses.includes(status) +} // #endregion diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js index 3090f33630ad6ee7c8c90509b0e6c06f7df38385..cd568ff1c9afd710040804d97e8e1d7988c5b17d 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js @@ -1,63 +1,52 @@ import React, { Fragment } from 'react' -import { reduxForm } from 'redux-form' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' import { required } from 'xpub-validators' -import { compose, withHandlers, withStateHandlers } from 'recompose' -import { withModal } from 'pubsweet-component-modal/src/components' -import { Button, FilePicker, Menu, ValidatedField } from '@pubsweet/ui' +import { Button, FilePicker, Menu, Spinner, ValidatedField } from '@pubsweet/ui' import { Row, + Text, Item, Label, FileItem, Textarea, ActionLink, - MultiAction, ContextualBox, ItemOverrideAlert, - withFetching, } from 'pubsweet-component-faraday-ui/src' -const options = [ - { value: 'a', label: 'a' }, - { value: 'b', label: 'b' }, - { value: 'c', label: 'c' }, - { value: 'd', label: 'd' }, -] - -const testFiles = [ - { - id: - '8dca903a-05b9-45ab-89b9-9cb99a9a29c6/02db6c5e-2938-45ac-a5ee-67ae63919bb2', - name: 'Supplementary File 1.jpg', - size: 59621, - originalName: 'Supplementary File 1.jpg', - }, - { - id: - '8dca903a-05b9-45ab-89b9-9cb99a9a29c6/5e69e3d9-7f9d-4e8d-b649-6e6a45658d75', - name: 'Supplementary File 2.docx', - size: 476862, - originalName: 'Supplementary File 2.docx', - }, -] - const ReviewerReportForm = ({ - file, + toggle, + hasNote, + addNote, addFile, expanded, - toggleMenu, + fileError, + isFetching, + removeNote, + removeFile, + previewFile, + downloadFile, handleSubmit, + fetchingError, + review = {}, + formValues = {}, + journal: { recommendations }, }) => ( - <ContextualBox label="Your report" startExpanded> + <ContextualBox + expanded={expanded} + highlight + label="Your report" + scrollIntoView + toggle={toggle} + > <Root> - <Row> - <ItemOverrideAlert vertical> + <Row justify="flex-start"> + <ItemOverrideAlert flex={0} vertical> <Label required>Recommendation</Label> <ValidatedField - component={input => <Menu {...input} options={options} />} + component={input => <Menu {...input} options={recommendations} />} name="recommendation" validate={[required]} /> @@ -67,9 +56,11 @@ const ReviewerReportForm = ({ <Row alignItems="center" justify="space-between" mt={1}> <Item> <Label required>Your report</Label> - <FilePicker onUpload={addFile}> - <ActionLink icon="plus">UPLOAD FILE</ActionLink> - </FilePicker> + {!formValues.file && ( + <FilePicker onUpload={addFile}> + <ActionLink icon="plus">UPLOAD FILE</ActionLink> + </FilePicker> + )} </Item> <Item justify="flex-end"> @@ -79,92 +70,74 @@ const ReviewerReportForm = ({ </Item> </Row> - <Row mb={1}> + <Row mb={1 / 2}> <ItemOverrideAlert vertical> - <ValidatedField - component={Textarea} - name="message" - validate={[required]} - /> + <ValidatedField component={Textarea} name="public" /> </ItemOverrideAlert> </Row> - <Row justify="flex-start"> - <Item flex={0}> - <FileItem item={file} /> - </Item> - </Row> + {formValues.file && ( + <Row justify="flex-start" mb={1}> + <Item flex={0}> + <FileItem + item={formValues.file} + onDelete={removeFile} + onDownload={downloadFile} + onPreview={previewFile} + /> + </Item> + </Row> + )} <Row alignItems="center"> - {expanded ? ( + {hasNote ? ( <Fragment> <Item> <Label>Confidential note for the Editorial Team</Label> </Item> <Item justify="flex-end"> - <ActionLink icon="x" onClick={toggleMenu}> + <ActionLink icon="x" onClick={removeNote}> Remove </ActionLink> </Item> </Fragment> ) : ( <Item> - <ActionLink onClick={toggleMenu}> + <ActionLink onClick={addNote}> Add Confidential note for the Editorial Team </ActionLink> </Item> )} </Row> - {expanded && ( + {hasNote && ( <Row> - <ItemOverrideAlert pr={2} vertical> - <ValidatedField component={Textarea} name="note" /> + <ItemOverrideAlert vertical> + <ValidatedField component={Textarea} name="confidential" /> </ItemOverrideAlert> </Row> )} - <Row justify="flex-end" mt={2}> - <Button onClick={handleSubmit} primary size="medium"> - Submit report - </Button> + {fetchingError && ( + <Row justify="flex-start"> + <Text error>{fetchingError}</Text> + </Row> + )} + + <Row justify="flex-end" mt={1}> + {isFetching ? ( + <Spinner /> + ) : ( + <Button onClick={handleSubmit} primary size="medium"> + Submit report + </Button> + )} </Row> </Root> </ContextualBox> ) -export default compose( - withFetching, - withModal(() => ({ - modalComponent: MultiAction, - })), - withStateHandlers( - { expanded: false, file: testFiles[0] }, - { - toggleMenu: ({ expanded }) => () => ({ expanded: !expanded }), - }, - ), - withHandlers({ - addFile: () => file => {}, - }), - reduxForm({ - form: 'reviewer-report', - onSubmit: ( - values, - dispatch, - { showModal, setFetching, onSubmitReport }, - ) => { - showModal({ - title: 'Ready to Submit your Report?', - subtitle: `Once submitted, the report can't be modified.`, - onConfirm: modalProps => - onSubmitReport(values, { ...modalProps, setFetching }), - confirmText: 'Submit report', - cancelText: 'Not yet', - }) - }, - }), -)(ReviewerReportForm) +export default ReviewerReportForm // #region styles const Root = styled.div` diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md deleted file mode 100644 index bf3998da8465ac23b794e7bccd57a3acb6071610..0000000000000000000000000000000000000000 --- a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md +++ /dev/null @@ -1,10 +0,0 @@ -Reviewer report contextual box. - -```js -<ReviewerReportForm - onSubmitReport={(values, { setFetching }) => { - console.log('submitting report', values) - setFetching(true) - }} -/> -``` diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index 0099f221526cd36d4d645e26628244ae482d9f8c..fb73c967cf33e4da9d7116f3f97a9bbde938c034 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -36,25 +36,30 @@ const ManuscriptLayout = ({ editorInChief, handlingEditors, createRecommendation, - hasResponseToReviewers, editorialRecommendations, journal = {}, collection = {}, fragment = {}, + changeForm, isFetching, formValues, - toggleAssignHE, heExpanded, - toggleHEResponse, - heResponseExpanded, onHEResponse, + toggleAssignHE, onInviteReviewer, - invitationsWithReviewers, + toggleHEResponse, + heResponseExpanded, + onReviewerResponse, onResendReviewerInvite, onRevokeReviewerInvite, toggleReviewerResponse, + invitationsWithReviewers, reviewerResponseExpanded, - onReviewerResponse, + pendingOwnRecommendation, + toggleReviewerRecommendations, + reviewerRecommendationExpanded, + // + shouldReview, }) => ( <Root pb={1}> {!isEmpty(collection) && !isEmpty(fragment) ? ( @@ -80,18 +85,26 @@ const ManuscriptLayout = ({ revokeInvitation={revokeHE} /> - <ReviewerReportForm - modalKey="reviewer-report" - project={collection} - version={fragment} - /> - <ManuscriptMetadata currentUser={currentUser} fragment={fragment} getSignedUrl={getSignedUrl} /> + {shouldReview && ( + <ReviewerReportForm + changeForm={changeForm} + expanded={reviewerRecommendationExpanded} + formValues={get(formValues, 'reviewerReport', {})} + modalKey="reviewer-report" + project={collection} + review={pendingOwnRecommendation} + toggle={toggleReviewerRecommendations} + token={get(currentUser, 'token')} + version={fragment} + /> + )} + {get(currentUser, 'isInvitedHE', false) && ( <ResponseToInvitation commentsOn="decline" diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 02c7b2b506621f788010c299062a2d271c856826..41814a95c6289dee72ed2ccddb50105051848ee7 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -2,9 +2,9 @@ import { connect } from 'react-redux' import { actions } from 'pubsweet-client' import { ConnectPage } from 'xpub-connect' import { withJournal } from 'xpub-journal' -import { getFormValues } from 'redux-form' import { withRouter } from 'react-router-dom' -import { head, get, isEmpty, isUndefined } from 'lodash' +import { head, get, isUndefined } from 'lodash' +import { getFormValues, change as changeForm } from 'redux-form' import { selectFragment, selectCollection, @@ -40,10 +40,11 @@ import { canInviteReviewers, pendingHEInvitation, currentUserIsReviewer, - canMakeRecommendation, parseCollectionDetails, pendingReviewerInvitation, canOverrideTechnicalChecks, + getOwnPendingRecommendation, + getOwnSubmittedRecommendation, getFragmentReviewerRecommendations, getInvitationsWithReviewersForFragment, } from 'pubsweet-component-faraday-selectors' @@ -86,6 +87,14 @@ export default compose( selectCollection(state, match.params.project), ), pendingHEInvitation: pendingHEInvitation(state, match.params.project), + pendingOwnRecommendation: getOwnPendingRecommendation( + state, + match.params.version, + ), + submittedOwnRecommendation: getOwnSubmittedRecommendation( + state, + match.params.version, + ), pendingReviewerInvitation: pendingReviewerInvitation( state, match.params.version, @@ -100,6 +109,7 @@ export default compose( ), }), { + changeForm, getSignedUrl, clearCustomError, assignHandlingEditor, @@ -120,6 +130,7 @@ export default compose( collection, currentUser, pendingHEInvitation, + pendingOwnRecommendation, pendingReviewerInvitation, }, ) => ({ @@ -128,21 +139,17 @@ export default compose( token: getUserToken(state), isHE: currentUserIs(state, 'isHE'), isEIC: currentUserIs(state, 'adminEiC'), - isReviewer: currentUserIsReviewer(state), + isReviewer: currentUserIsReviewer(state, get(fragment, 'id', '')), isInvitedHE: !isUndefined(pendingHEInvitation), isInvitedToReview: !isUndefined(pendingReviewerInvitation), permissions: { canAssignHE: canAssignHE(state, match.params.project), canInviteReviewers: canInviteReviewers(state, collection), + canMakeRecommendation: !isUndefined(pendingOwnRecommendation), canMakeRevision: canMakeRevision(state, collection, fragment), canMakeDecision: canMakeDecision(state, collection, fragment), canEditManuscript: canEditManuscript(state, collection, fragment), canOverrideTechChecks: canOverrideTechnicalChecks(state, collection), - canMakeRecommendation: canMakeRecommendation( - state, - collection, - fragment, - ), }, }, isFetching: { @@ -151,6 +158,7 @@ export default compose( }, formValues: { eicDecision: getFormValues('eic-decision')(state), + reviewerReport: getFormValues('reviewerReport')(state), responseToInvitation: getFormValues('answer-invitation')(state), }, invitationsWithReviewers: getInvitationsWithReviewersForFragment( @@ -371,12 +379,22 @@ export default compose( toggleReviewerResponse: toggle, reviewerResponseExpanded: expanded, })), + fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ + toggleReviewerRecommendations: toggle, + reviewerRecommendationExpanded: expanded, + })), + withProps(({ currentUser, submittedOwnRecommendation }) => ({ + shouldReview: + get(currentUser, 'isReviewer', false) && + isUndefined(submittedOwnRecommendation), + })), lifecycle({ componentDidMount() { const { match, history, location, + shouldReview, setEditorInChief, clearCustomError, hasManuscriptFailure, @@ -409,11 +427,10 @@ export default compose( if (isInvitedToReview) { this.props.toggleReviewerResponse() } + + if (shouldReview) { + this.props.toggleReviewerRecommendations() + } }, }), - withProps(({ fragment }) => ({ - hasResponseToReviewers: - !isEmpty(get(fragment, 'files.responseToReviewers')) || - get(fragment, 'commentsToReviewers'), - })), )(ManuscriptLayout) diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.js b/packages/component-manuscript/src/components/ReviewerReportForm.js index 64db430c37eae91da3ef0653d8ae1d2a7dd0ed21..cf886b32a4f4b10bce0de9796e98f3a2ba3da049 100644 --- a/packages/component-manuscript/src/components/ReviewerReportForm.js +++ b/packages/component-manuscript/src/components/ReviewerReportForm.js @@ -1,34 +1,18 @@ -import React, { Fragment } from 'react' import { get } from 'lodash' -import { connect } from 'react-redux' -import styled from 'styled-components' -import { th } from '@pubsweet/ui-toolkit' +import { reduxForm } from 'redux-form' import { withJournal } from 'xpub-journal' -import { required } from 'xpub-validators' import { withModal } from 'pubsweet-component-modal/src/components' import { compose, withHandlers, withProps, withState } from 'recompose' -import { reduxForm, getFormValues, change as changeForm } from 'redux-form' -import { Menu, Button, FilePicker, Spinner, ValidatedField } from '@pubsweet/ui' import { - Row, - Item, - Text, - Label, - FileItem, - Textarea, - ActionLink, MultiAction, - ContextualBox, - ItemOverrideAlert, + ReviewerReportForm, handleError, withFetching, withFilePreview, withFileDownload, } from 'pubsweet-component-faraday-ui' -import { getOwnRecommendation } from 'pubsweet-component-faraday-selectors' - import { uploadFile, deleteFile, @@ -42,133 +26,10 @@ import { parseReviewResponseToForm, } from './utils' -const ReviewerReportForm = ({ - addNote, - addFile, - expanded, - fileError, - isFetching, - removeNote, - removeFile, - previewFile, - downloadFile, - handleSubmit, - fetchingError, - review = {}, - formValues = {}, - journal: { recommendations }, -}) => ( - <ContextualBox label="Your report" startExpanded> - <Root> - <Row justify="flex-start"> - <ItemOverrideAlert flex={0} vertical> - <Label required>Recommendation</Label> - <ValidatedField - component={input => <Menu {...input} options={recommendations} />} - name="recommendation" - validate={[required]} - /> - </ItemOverrideAlert> - </Row> - - <Row alignItems="center" justify="space-between" mt={1}> - <Item> - <Label required>Your report</Label> - {!formValues.file && ( - <FilePicker onUpload={addFile}> - <ActionLink icon="plus">UPLOAD FILE</ActionLink> - </FilePicker> - )} - </Item> - - <Item justify="flex-end"> - <ActionLink to="https://about.hindawi.com/authors/peer-review-at-hindawi/"> - Hindawi Reviewer Guidelines - </ActionLink> - </Item> - </Row> - - <Row mb={1 / 2}> - <ItemOverrideAlert vertical> - <ValidatedField component={Textarea} name="public" /> - </ItemOverrideAlert> - </Row> - - {formValues.file && ( - <Row justify="flex-start" mb={1}> - <Item flex={0}> - <FileItem - item={formValues.file} - onDelete={removeFile} - onDownload={downloadFile} - onPreview={previewFile} - /> - </Item> - </Row> - )} - - <Row alignItems="center"> - {expanded ? ( - <Fragment> - <Item> - <Label>Confidential note for the Editorial Team</Label> - </Item> - <Item justify="flex-end"> - <ActionLink icon="x" onClick={removeNote}> - Remove - </ActionLink> - </Item> - </Fragment> - ) : ( - <Item> - <ActionLink onClick={addNote}> - Add Confidential note for the Editorial Team - </ActionLink> - </Item> - )} - </Row> - - {expanded && ( - <Row> - <ItemOverrideAlert vertical> - <ValidatedField component={Textarea} name="confidential" /> - </ItemOverrideAlert> - </Row> - )} - - {fetchingError && ( - <Row justify="flex-start"> - <Text error>{fetchingError}</Text> - </Row> - )} - - <Row justify="flex-end" mt={1}> - {isFetching ? ( - <Spinner /> - ) : ( - <Button onClick={handleSubmit} primary size="medium"> - Submit report - </Button> - )} - </Row> - </Root> - </ContextualBox> -) - // #region export export default compose( withJournal, withFetching, - connect( - (state, { version, ...rest }) => ({ - review: getOwnRecommendation(state, version.id), - token: get(state, 'currentUser.user.token', ''), - formValues: getFormValues('reviewerReport')(state), - }), - { - changeForm, - }, - ), withProps(({ review = {}, formValues = {} }) => ({ getSignedUrl, initialValues: parseReviewResponseToForm(review), @@ -181,17 +42,17 @@ export default compose( modalComponent: MultiAction, })), withState( - 'expanded', - 'setExpanded', + 'hasNote', + 'setNote', ({ review }) => get(review, 'comments', []).length === 2, ), withHandlers({ - addNote: ({ setExpanded }) => () => { - setExpanded(true) + addNote: ({ setNote }) => () => { + setNote(true) }, - removeNote: ({ setExpanded, changeForm }) => () => { + removeNote: ({ setNote, changeForm }) => () => { changeForm('reviewerReport', 'confidential', '') - setExpanded(false) + setNote(false) }, addFile: ({ version, changeForm, setFetching, setError }) => file => { setFetching(true) @@ -230,10 +91,3 @@ export default compose( }), )(ReviewerReportForm) // #endregion - -// #region styled-components -const Root = styled.div` - background-color: ${th('colorBackgroundHue2')}; - padding: calc(${th('gridUnit')} * 2); -` -// #endregion diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js index 28d7fef24154ea257e0779811c6db44bfc73fe7f..632608f2d5bc2bb6db381efccda820c8a8491be0 100644 --- a/packages/component-manuscript/src/components/utils.js +++ b/packages/component-manuscript/src/components/utils.js @@ -1,5 +1,6 @@ import moment from 'moment' import { + has, get, find, omit, @@ -8,6 +9,7 @@ import { mergeWith, capitalize, } from 'lodash' +import { change as changeForm } from 'redux-form' import { actions } from 'pubsweet-client/src' import { handleError } from 'pubsweet-component-faraday-ui' @@ -121,15 +123,15 @@ export const parseReviewRequest = (review = {}) => { const comments = [ { public: true, - files: [get(review, 'file', {})], - content: get(review, 'public', ''), + files: has(review, 'file') ? [get(review, 'file')] : [], + content: get(review, 'public'), }, ] if (get(review, 'confidential', '')) { comments.push({ public: false, - content: review.confidential || undefined, + content: get(review, 'confidential'), files: [], }) } @@ -176,6 +178,7 @@ const onChange = (values, dispatch, { project, version }) => { recommendation: omit(newValues, 'id'), }).then( r => { + dispatch(changeForm('reviewerReport', 'id', r.id)) dispatch(autosaveSuccess(Date.now())) return r },