diff --git a/packages/component-manuscript/src/components/Authors.js b/packages/component-manuscript/src/components/Authors.js index dc07b1adc938e8f8d71ca3f49a6bf55f70ee4c83..bd8462bca91a3e0ddb2c56e7c5b72ec5c84bbeb7 100644 --- a/packages/component-manuscript/src/components/Authors.js +++ b/packages/component-manuscript/src/components/Authors.js @@ -17,7 +17,7 @@ const TR = ({ {isSubmitting && <AuthorStatus>SA</AuthorStatus>} {isCorresponding && !isSubmitting && <AuthorStatus>CA</AuthorStatus>} </td> - <td>{email || 'N/A'}</td> + <td>{email || ''}</td> <td>{affiliation}</td> </Row> ) @@ -27,7 +27,7 @@ const Authors = ({ authors }) => ( <thead> <tr> <td colSpan="2">Full Name</td> - <td>Email</td> + <td>{authors[0].email ? 'Email' : ''}</td> <td>Affiliation</td> </tr> </thead> diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.js b/packages/component-manuscript/src/components/ReviewerReportForm.js new file mode 100644 index 0000000000000000000000000000000000000000..2a62ea58832a86e892f27406b822f32aeb43d7eb --- /dev/null +++ b/packages/component-manuscript/src/components/ReviewerReportForm.js @@ -0,0 +1,280 @@ +import React, { Fragment } from 'react' +import { connect } from 'react-redux' +import { required } from 'xpub-validators' +import styled, { css } from 'styled-components' +import { compose, withHandlers, withProps } from 'recompose' +import { th, Menu, ValidatedField, Icon, Button, Spinner } from '@pubsweet/ui' +import { + reduxForm, + isSubmitting, + change as changeForm, + getFormValues, +} from 'redux-form' +import AutosaveIndicator from 'pubsweet-component-wizard/src/components/AutosaveIndicator' +import { + autosaveRequest, + autosaveSuccess, +} from 'pubsweet-component-wizard/src/redux/autosave' + +import { parseReviewResponseToForm, parseReviewRequest } from './utils' + +const guidelinesLink = + 'https://about.hindawi.com/authors/peer-review-at-hindawi/' +const options = [ + { + value: 'publish', + label: 'Publish unaltered', + }, + { + value: 'major', + label: 'Consider after major revision', + }, + { + value: 'minor', + label: 'Consider after major revision', + }, + { + value: 'reject', + label: 'Reject', + }, +] + +const review = { + id: 'revuewiuuuid', + userId: 'uuuuuuid', + recommendation: 'publish', + recommendationType: 'review', + comments: [ + { + content: 'Here is public text', + public: true, + files: [], + }, + { + content: 'Here is PRIVATE text', + public: false, + files: [], + }, + ], +} + +const ReviewerReportForm = ({ + isSubmitting, + changeField, + handleSubmit, + formValues = {}, + initialValues, +}) => ( + <Root> + <Row> + <Label>Recommendation*</Label> + <ActionLink href={guidelinesLink} target="_blank"> + Hindawi Reviewer Guidelines + </ActionLink> + </Row> + <Row> + <ValidatedField + component={input => ( + <Menu + {...input} + inline + onChange={v => changeField('recommendation', v)} + options={options} + placeholder="Select" + /> + )} + name="recommendation" + validate={[required]} + /> + </Row> + <Spacing /> + <Row> + <Label> + Report <ActionText left={12}>Upload file</ActionText> + </Label> + </Row> + <Row> + <FullWidth> + <ValidatedField + component={input => ( + <Textarea + {...input} + hasError={input.validationStatus === 'error'} + onChange={e => changeField('public', e.target.value)} + rows={6} + /> + )} + name="public" + validate={[required]} + /> + </FullWidth> + </Row> + {formValues.hasConfidential ? ( + <Fragment> + <Row> + <Label> + Note for the editorial team <i>Not shared with the author</i> + </Label> + <ActionTextIcon onClick={() => changeField('hasConfidential', false)}> + <Icon primary size={3}> + x + </Icon> + Remove + </ActionTextIcon> + </Row> + <Row> + <FullWidth> + <ValidatedField + component={input => ( + <Textarea + {...input} + hasError={input.validationStatus === 'error'} + onChange={e => changeField('confidential', e.target.value)} + rows={6} + /> + )} + name="confidential" + validate={[required]} + /> + </FullWidth> + </Row> + </Fragment> + ) : ( + <Row> + <ActionText onClick={() => changeField('hasConfidential', true)}> + Add confidential note for the Editorial Team + </ActionText> + </Row> + )} + + <Spacing /> + <Row> + {isSubmitting ? ( + <Spinner size={4} /> + ) : ( + <ActionButton onClick={handleSubmit}> Submit report </ActionButton> + )} + <AutosaveIndicator formName="reviewerReport" /> + </Row> + </Root> +) + +// To be removed +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + +export default compose( + connect( + state => ({ + formValues: getFormValues('reviewerReport')(state), + isSubmitting: isSubmitting('reviewerReport')(state), + }), + { changeForm, getFormValues }, + ), + withProps(() => ({ + initialValues: parseReviewResponseToForm({}), + })), + withHandlers({ + changeField: ({ changeForm }) => (field, value) => { + changeForm('reviewerReport', field, value) + }, + }), + reduxForm({ + form: 'reviewerReport', + enableReinitialize: true, + forceUnregisterOnUnmount: true, + onChange: (values, dispatch) => { + dispatch(autosaveRequest()) + sleep(1000).then(() => dispatch(autosaveSuccess(new Date()))) + }, + onSubmit: (values, dispatch, { isSubmitting }) => + sleep(1000).then(() => { + // TODO: link to backend + const review = parseReviewRequest(values) + window.alert(`You submitted:\n\n${JSON.stringify(review, null, 2)}`) + }), + }), +)(ReviewerReportForm) + +// #region styled-components + +const defaultText = css` + color: ${th('colorPrimary')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` +const Root = styled.div` + display: flex; + flex-direction: column; + margin: auto; + [role='listbox'] { + min-width: 280px; + } +` + +const Label = styled.div` + ${defaultText}; + text-transform: uppercase; + i { + text-transform: none; + margin-left: ${th('gridUnit')}; + } +` + +const ActionText = styled.span` + ${defaultText}; + text-decoration: underline; + cursor: pointer; + margin-left: ${({ left }) => left || 0}px; +` + +const ActionTextIcon = styled(ActionText)` + display: flex; + align-items: center; +` +const ActionLink = styled.a` + ${defaultText}; +` + +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')}; +` + +const Spacing = styled.div` + margin-top: ${th('gridUnit')}; + flex: 1; +` + +const FullWidth = styled.div` + flex: 1; + > div { + flex: 1; + } +` + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; + flex: 1; + box-sizing: border-box; + flex-wrap: wrap; + justify-content: space-between; +` + +const ActionButton = styled(Button)` + ${defaultText}; + align-items: center; + background-color: ${th('colorPrimary')}; + color: ${th('colorTextReverse')}; + display: flex; + padding: 4px 8px; + text-align: center; + height: calc(${th('subGridUnit')}*5); + text-transform: uppercase; +` +// #endregion diff --git a/packages/component-manuscript/src/components/ReviewsAndReports.js b/packages/component-manuscript/src/components/ReviewsAndReports.js index 6a8ead1f0fea8fa9f44965e425a3c6fe340e497b..15fa55f80fddbe236e0cb42015ff8e73145f2d3c 100644 --- a/packages/component-manuscript/src/components/ReviewsAndReports.js +++ b/packages/component-manuscript/src/components/ReviewsAndReports.js @@ -5,6 +5,7 @@ import styled from 'styled-components' import { compose, withHandlers, lifecycle } from 'recompose' import { ReviewerBreakdown } from 'pubsweet-components-faraday/src/components/Invitations' import ReviewersDetailsList from 'pubsweet-components-faraday/src/components/Reviewers/ReviewersDetailsList' +import ReviewerReportForm from 'pubsweet-component-manuscript/src/components/ReviewerReportForm' import { selectReviewers, selectFetchingReviewers, @@ -54,9 +55,9 @@ const ReviewsAndReports = ({ </Root> )} {isReviewer && ( - <Root> + <Root id="review-report"> <Expandable label="Your Report" startExpanded> - <div>Form here, to be implemented</div> + <ReviewerReportForm /> </Expandable> </Root> )} diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js index 0fb7764d1db83459daa972501fdd4b3bc1afe763..17ad97ee832b37c0a6daa0b3a85c45b7be7e4d2b 100644 --- a/packages/component-manuscript/src/components/utils.js +++ b/packages/component-manuscript/src/components/utils.js @@ -1,5 +1,5 @@ import moment from 'moment' -import { get, find, capitalize } from 'lodash' +import { get, find, capitalize, omit } from 'lodash' export const parseTitle = version => { const title = get(version, 'metadata.title') @@ -80,3 +80,38 @@ export const redirectToError = redirectFn => err => { redirectFn('/error-page', 'Oops! Something went wrong.') } } + +export const parseReviewResponseToForm = (review = {}) => { + const comments = review.comments || [] + const publicComment = comments.find(c => c.public) + const privateComment = comments.find(c => !c.public) + return { + ...review, + public: get(publicComment, 'content'), + files: get(publicComment, 'files'), + confidential: get(privateComment, 'content'), + hasConfidential: !!get(privateComment, 'content'), + } +} + +export const parseReviewRequest = (review = {}) => { + const comments = [ + { + public: true, + content: review.public, + files: review.files || [], + }, + ] + + if (review.hasConfidential) { + comments.push({ + public: false, + content: review.confidential, + files: [], + }) + } + return { + ...omit(review, ['public', 'confidential', 'hasConfidential', 'files']), + comments, + } +} diff --git a/packages/component-wizard/src/redux/autosave.js b/packages/component-wizard/src/redux/autosave.js index a9a8bb816c086884dd7031aeb99dcd6a041e8392..e6db38ebc19e5a9c3799524faf72824bde2e4088 100644 --- a/packages/component-wizard/src/redux/autosave.js +++ b/packages/component-wizard/src/redux/autosave.js @@ -43,7 +43,7 @@ export default (state = initialState, action) => { case 'UPDATE_FRAGMENT_SUCCESS': return { ...initialState, - lastUpdate: action.receivedAt, + lastUpdate: action.receivedAt || action.lastUpdate, } default: return state diff --git a/packages/component-wizard/src/redux/conversion.js b/packages/component-wizard/src/redux/conversion.js index becacf4146503472797087aa17aafebd4efdf257..a4043ca5dd365aadc19f2f7a9e7908e89a592c6a 100644 --- a/packages/component-wizard/src/redux/conversion.js +++ b/packages/component-wizard/src/redux/conversion.js @@ -58,6 +58,9 @@ export const createDraftSubmission = history => (dispatch, getState) => { version: 1, }), ).then(({ fragment }) => { + if (!fragment.id) { + throw new Error('Failed to create a project') + } const route = `/projects/${collection.id}/versions/${fragment.id}/submit` if (!currentUser.admin) { addSubmittingAuthor(currentUser, collection.id) diff --git a/packages/components-faraday/src/redux/index.js b/packages/components-faraday/src/redux/index.js index ddaa6385d75eba8dbcc2f2abfe25f757f6021591..12a9544f097bb4a8b08317dad23152255879d3fe 100644 --- a/packages/components-faraday/src/redux/index.js +++ b/packages/components-faraday/src/redux/index.js @@ -2,3 +2,4 @@ export { default as authors } from './authors' export { default as editors } from './editors' export { default as files } from './files' export { default as reviewers } from './reviewers' +export { default as recommendations } from './recommendations' diff --git a/packages/components-faraday/src/redux/recommendations.js b/packages/components-faraday/src/redux/recommendations.js new file mode 100644 index 0000000000000000000000000000000000000000..3a8cf0213622cf40184dc55569490d94aa865abd --- /dev/null +++ b/packages/components-faraday/src/redux/recommendations.js @@ -0,0 +1,88 @@ +import { + get as apiGet, + create, + remove, + update, +} from 'pubsweet-client/src/helpers/api' + +const REQUEST = 'recommendations/REQUEST' +const ERROR = 'recommendations/ERROR' + +const GET_RECOMMENDATIONS_SUCCESS = 'recommendations/GET_SUCCESS' +const GET_RECOMMENDATION_SUCCESS = 'recommendations/GET_ITEM_SUCCESS' +const UPDATE_RECOMMENDATION_SUCCESS = 'recommendations/UPDATE_SUCCESS' + +export const recommendationsRequest = () => ({ + type: REQUEST, +}) + +export const recommendationsError = error => ({ + type: ERROR, + error, +}) + +export const getRecommendationsSuccess = recommendations => ({ + type: GET_RECOMMENDATIONS_SUCCESS, + payload: { recommendations }, +}) + +export const getRecommendationSuccess = recommendation => ({ + type: GET_RECOMMENDATION_SUCCESS, + payload: { recommendation }, +}) + +export const updateRecommendationSuccess = recommendation => ({ + type: UPDATE_RECOMMENDATION_SUCCESS, + payload: { recommendation }, +}) + +// Actions + +// State +const initialState = { + fetching: false, + error: null, + recommendations: [], + recommendation: {}, +} + +export default (state = initialState, action = {}) => { + switch (action.type) { + case REQUEST: + return { + ...state, + fetching: true, + recommendations: [], + recommendation: {}, + } + case ERROR: + return { + ...state, + fetching: false, + error: action.error, + } + case GET_RECOMMENDATIONS_SUCCESS: + return { + ...state, + fetching: false, + error: null, + recommendations: action.payload.recommendations, + } + case GET_RECOMMENDATION_SUCCESS: + return { + ...state, + fetching: false, + error: null, + recommendation: action.payload.recommendation, + } + case UPDATE_RECOMMENDATION_SUCCESS: + return { + ...state, + fetching: false, + error: null, + recommendation: action.payload.recommendation, + } + default: + return state + } +}