diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index 6c9ebc9280edf6b774437264c1fce3a5324f7245..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) => { @@ -8,18 +8,6 @@ export const isHEToManuscript = (state, collectionId) => { return get(collection, 'handlingEditor.id') === currentUserId } -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 currentUserIs = ({ currentUser: { user } }, role) => { const isAdmin = get(user, 'admin') const isEic = get(user, 'editorInChief') @@ -228,3 +216,49 @@ export const getInvitationsWithReviewersForFragment = (state, fragmentId) => ), })) .value() + +// #region Editorial and reviewer recommendations +export const getFragmentRecommendations = (state, fragmentId) => + get(state, `fragments.${fragmentId}.recommendations`, []) + +export const getFragmentReviewerRecommendations = (state, fragmentId) => + getFragmentRecommendations(state, fragmentId).filter( + r => r.recommendationType === 'review', + ) + +const getOwnRecommendations = (state, fragmentId) => + chain(state) + .get(`fragments.${fragmentId}.recommendations`, []) + .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/File.js b/packages/component-faraday-ui/src/File.js index 99a4afea1b32dd010d9d0efe20c42aa4599edc45..4c6014d3ef4286bc0559282c6cfbc49846704f42 100644 --- a/packages/component-faraday-ui/src/File.js +++ b/packages/component-faraday-ui/src/File.js @@ -56,6 +56,7 @@ const FileItem = ({ ml={1} mr={1} onClick={onPreview} + pt={1 / 2} secondary /> )} @@ -65,6 +66,7 @@ const FileItem = ({ ml={hasPreview ? 0 : 1} mr={1} onClick={onDownload} + pt={1 / 2} secondary /> {hasDelete && ( @@ -73,6 +75,7 @@ const FileItem = ({ iconSize={2} mr={1} onClick={onDelete} + pt={1 / 2} secondary /> )} diff --git a/packages/component-faraday-ui/src/FileSection.js b/packages/component-faraday-ui/src/FileSection.js index 1ea86ac790bd83c2f61f567f51654423f70e9532..6d799cfa855732275e6b783feb16b1d6b3a78c80 100644 --- a/packages/component-faraday-ui/src/FileSection.js +++ b/packages/component-faraday-ui/src/FileSection.js @@ -1,7 +1,7 @@ import React from 'react' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' -import { FilePicker } from '@pubsweet/ui' +import { FilePicker, Spinner } from '@pubsweet/ui' import { compose, withState, withHandlers, withProps } from 'recompose' import { radiusHelpers } from './styledHelpers' @@ -36,6 +36,7 @@ const FileSection = ({ required, moveItem, maxFiles, + isFetching, isFileItemOver, canDropFileItem, connectFileDrop, @@ -60,19 +61,23 @@ const FileSection = ({ <Row alignItems="center"> <Item> <Label required={required}>{title}</Label> - <FilePicker - allowedFileExtensions={allowedFileExtensions} - disabled={files.length >= maxFiles} - onUpload={onFilePick} - > - <ActionLink + {isFetching ? ( + <Spinner /> + ) : ( + <FilePicker + allowedFileExtensions={allowedFileExtensions} disabled={files.length >= maxFiles} - icon="plus" - size="small" + onUpload={onFilePick} > - UPLOAD FILE - </ActionLink> - </FilePicker> + <ActionLink + disabled={files.length >= maxFiles} + icon="plus" + size="small" + > + UPLOAD FILE + </ActionLink> + </FilePicker> + )} </Item> {supportedFormats && ( <Item justify="flex-end"> diff --git a/packages/component-faraday-ui/src/ReviewerReport.js b/packages/component-faraday-ui/src/ReviewerReport.js index 401f3818edffd6781810f1e5f2e7c3456d984f32..212ded253217b36bc07f1dda42ba9998adf6c14d 100644 --- a/packages/component-faraday-ui/src/ReviewerReport.js +++ b/packages/component-faraday-ui/src/ReviewerReport.js @@ -1,4 +1,6 @@ -import React from 'react' +import React, { Fragment } from 'react' +import { get } from 'lodash' +import { withProps } from 'recompose' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' import { DateParser } from '@pubsweet/ui' @@ -6,14 +8,14 @@ import { DateParser } from '@pubsweet/ui' import { Label, Item, FileItem, Row, Text } from './' const ReviewerReport = ({ - report: { - report, - files = [], - submittedOn, - recommendation, - confidentialNote, - reviewer: { fullName, reviewerNumber }, - }, + onPreview, + onDownload, + reportFile, + publicReport, + privateReport, + recommendation, + showOwner = false, + report: { submittedOn }, }) => ( <Root> <Row justify="space-between" mb={2}> @@ -23,42 +25,64 @@ const ReviewerReport = ({ </Item> <Item justify="flex-end"> - <Text>{fullName}</Text> - <Text customId ml={1} mr={1}> - {`Reviewer ${reviewerNumber}`} - </Text> + {showOwner && ( + <Fragment> + <Text>Reviewer name</Text> + <Text customId ml={1} mr={1}> + {`Reviewer ${1}`} + </Text> + </Fragment> + )} <DateParser timestamp={submittedOn}> {date => <Text>{date}</Text>} </DateParser> </Item> </Row> - <Row mb={2}> - <Item vertical> - <Label mb={1 / 2}>Report</Label> - <Text>{report}</Text> - </Item> - </Row> - - <Label mb={1 / 2}>Files</Label> - <Row justify="flex-start" mb={2}> - {files.map(file => ( - <Item flex={0} key={file.id} mr={1}> - <FileItem item={file} /> + {publicReport && ( + <Row mb={2}> + <Item vertical> + <Label mb={1 / 2}>Report</Label> + <Text>{publicReport}</Text> </Item> - ))} - </Row> + </Row> + )} - <Row mb={2}> - <Item vertical> - <Label mb={1 / 2}>Confidential note for the Editorial Team</Label> - <Text>{confidentialNote}</Text> - </Item> - </Row> + {reportFile && ( + <Fragment> + <Label mb={1 / 2}>Report file</Label> + <Row justify="flex-start" mb={2}> + <Item flex={0} mr={1}> + <FileItem + item={reportFile} + onDownload={onDownload} + onPreview={onPreview} + /> + </Item> + </Row> + </Fragment> + )} + + {privateReport && ( + <Row mb={2}> + <Item vertical> + <Label mb={1 / 2}>Confidential note for the Editorial Team</Label> + <Text>{privateReport}</Text> + </Item> + </Row> + )} </Root> ) -export default ReviewerReport +export default withProps(({ report, journal: { recommendations = [] } }) => ({ + recommendation: get( + recommendations.find(r => r.value === report.recommendation), + 'label', + ), + reportFile: get(report, 'comments.0.files.0'), + publicReport: get(report, 'comments.0.content'), + privateReport: get(report, 'comments.1.content'), +}))(ReviewerReport) // #region styles const Root = styled.div` diff --git a/packages/component-faraday-ui/src/ReviewerReport.md b/packages/component-faraday-ui/src/ReviewerReport.md index e16f2bc1a28bc427029e6c9388d8f8288f031112..927701f99f9d8375c2e4115050706be80e313270 100644 --- a/packages/component-faraday-ui/src/ReviewerReport.md +++ b/packages/component-faraday-ui/src/ReviewerReport.md @@ -1,26 +1,61 @@ Reviewer report. ```js -<ReviewerReport - report={{ - submittedOn: Date.now(), - recommendation: 'Reject', - report: `Of all of the celestial bodies that capture our attention and - fascination as astronomers, none has a greater influence on life on - planet Earth than it’s own satellite, the moon. When you think about - it, we regard the moon with such powerful significance that unlike the - moons of other planets which we give names, we only refer to our one - and only orbiting orb as THE moon. It is not a moon. To us, it is the - one and only moon.`, - reviewer: { - fullName: 'Kenny Hudson', - reviewerNumber: 1, +const report = { + id: '71effdc0-ccb1-4ea9-9422-dcc9f8713347', + userId: '9ac5b5b5-252c-4933-9e66-72ec7c644a5c', + comments: [ + { + files: [ + { + id: + '5c0a233b-2569-443b-8110-ef98a18a60a4/2cac524e-0259-45fb-ad3c-9ebc94af8acc', + name: '1508309142.png', + size: 35249, + originalName: '1508309142.png', + }, + ], + public: true, + content: 'Arata foarte bine', }, - confidentialNote: `First 10 pages feel very familiar, you should check for plagarism.`, - files: [ - { id: 'file1', name: 'file1.pdf', size: 12356 }, - { id: 'file2', name: 'file2.pdf', size: 76421 }, - ], - }} + { + files: [], + public: false, + content: 'o da bine baiatul', + }, + ], + createdOn: 1538053564396, + updatedOn: 1538053600643, + submittedOn: 1538053600624, + recommendation: 'publish', + recommendationType: 'review', +} + +const journal = { + recommendations: [ + { + value: 'publish', + label: 'Publish unaltered', + }, + { + value: 'major', + label: 'Consider after major revision', + }, + { + value: 'minor', + label: 'Consider after minor revision', + }, + { + value: 'reject', + label: 'Reject', + }, + ], +} + +;<ReviewerReport + journal={journal} + report={report} + showOwner + onPreview={file => console.log('preview file', file)} /> ``` diff --git a/packages/component-faraday-ui/src/Textarea.js b/packages/component-faraday-ui/src/Textarea.js index bd385619a2ba0f5c56ec6cd321427f0c11d8cf34..122cf5a685a30977984b4045293b0843093db183 100644 --- a/packages/component-faraday-ui/src/Textarea.js +++ b/packages/component-faraday-ui/src/Textarea.js @@ -32,9 +32,5 @@ const Textarea = styled.textarea` } ` -Textarea.defaultProps = { - mb: 1, -} - /** @component */ export default Textarea diff --git a/packages/component-faraday-ui/src/WizardFiles.js b/packages/component-faraday-ui/src/WizardFiles.js index 14b5fcb14ee7d139959eba4ee5cffa2f9d0ed937..5c08242dd168384602c091f6b982435afd147169 100644 --- a/packages/component-faraday-ui/src/WizardFiles.js +++ b/packages/component-faraday-ui/src/WizardFiles.js @@ -2,12 +2,17 @@ import React, { Fragment } from 'react' import { get } from 'lodash' import { compose, withState, withHandlers } from 'recompose' -import { FileSection, SortableList } from './' -import { withFileDownload, withFilePreview } from './helpers' +import { + FileSection, + SortableList, + withFilePreview, + withFileDownload, +} from './' const WizardFiles = ({ files, addFile, + fetching, moveFile, deleteFile, changeList, @@ -19,6 +24,7 @@ const WizardFiles = ({ allowedFileExtensions={['pdf', 'doc', 'docx', 'txt', 'rdf', 'odt']} changeList={changeList} files={get(files, 'manuscripts', [])} + isFetching={get(fetching, 'manuscripts', false)} isFirst listId="manuscripts" maxFiles={1} @@ -35,6 +41,7 @@ const WizardFiles = ({ allowedFileExtensions={['pdf', 'doc', 'docx', 'txt', 'rdf', 'odt']} changeList={changeList} files={get(files, 'coverLetter', [])} + isFetching={get(fetching, 'coverLetter', false)} listId="coverLetter" maxFiles={1} moveItem={moveFile('coverLetter')} @@ -48,6 +55,7 @@ const WizardFiles = ({ <FileSection changeList={changeList} files={get(files, 'supplementary', [])} + isFetching={get(fetching, 'supplementary', false)} isLast listId="supplementary" moveItem={moveFile('supplementary')} @@ -65,33 +73,67 @@ export default compose( withFilePreview, withFileDownload, withState('files', 'setFiles', ({ files }) => files), + withState('fetching', 'setFilesFetching', { + manuscripts: false, + coverLetter: false, + supplementary: false, + }), withHandlers({ setFormFiles: ({ changeForm, setFiles }) => files => { setFiles(files) changeForm('submission', 'files', files) }, + setFilesFetching: ({ setFilesFetching }) => (type, value) => { + setFilesFetching(p => ({ + ...p, + [type]: value, + })) + }, }), withHandlers({ - addFile: ({ uploadFile, files, version, setFormFiles }) => type => file => { - uploadFile(file, type, version).then(f => { - const newFiles = { - ...files, - [type]: [...files[type], f], - } - setFormFiles(newFiles) - }) + addFile: ({ + files, + version, + uploadFile, + setFormFiles, + setFilesFetching, + }) => type => file => { + setFilesFetching(type, true) + uploadFile({ file, type, fragment: version }) + .then(f => { + const newFiles = { + ...files, + [type]: [...files[type], f], + } + setFormFiles(newFiles) + setFilesFetching(type, false) + }) + .catch(() => { + setFilesFetching(type, false) + }) }, downloadFile: ({ downloadFile, token }) => file => { downloadFile(file) }, - deleteFile: ({ deleteFile, files, setFormFiles }) => type => file => { - deleteFile(file.id, type).then(() => { - const newFiles = { - ...files, - [type]: files[type].filter(f => f.id !== file.id), - } - setFormFiles(newFiles) - }) + deleteFile: ({ + deleteFile, + files, + setFormFiles, + setFilesFetching, + }) => type => file => { + setFilesFetching(type, true) + deleteFile(file.id, type) + .then(() => { + const newFiles = { + ...files, + [type]: files[type].filter(f => f.id !== file.id), + } + setFormFiles(newFiles) + setFilesFetching(type, false) + }) + .catch(() => { + setFilesFetching(type, false) + }) }, moveFile: ({ files, setFormFiles }) => type => (dragIndex, hoverIndex) => { const newFiles = { diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js new file mode 100644 index 0000000000000000000000000000000000000000..cd568ff1c9afd710040804d97e8e1d7988c5b17d --- /dev/null +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js @@ -0,0 +1,147 @@ +import React, { Fragment } from 'react' +import styled from 'styled-components' +import { th } from '@pubsweet/ui-toolkit' +import { required } from 'xpub-validators' +import { Button, FilePicker, Menu, Spinner, ValidatedField } from '@pubsweet/ui' + +import { + Row, + Text, + Item, + Label, + FileItem, + Textarea, + ActionLink, + ContextualBox, + ItemOverrideAlert, +} from 'pubsweet-component-faraday-ui/src' + +const ReviewerReportForm = ({ + toggle, + hasNote, + addNote, + addFile, + expanded, + fileError, + isFetching, + removeNote, + removeFile, + previewFile, + downloadFile, + handleSubmit, + fetchingError, + review = {}, + formValues = {}, + journal: { recommendations }, +}) => ( + <ContextualBox + expanded={expanded} + highlight + label="Your report" + scrollIntoView + toggle={toggle} + > + <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"> + {hasNote ? ( + <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> + + {hasNote && ( + <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> +) + +export default ReviewerReportForm + +// #region styles +const Root = styled.div` + background-color: ${th('colorBackgroundHue2')}; + padding: calc(${th('gridUnit')} * 2); +` +// #endregion diff --git a/packages/component-faraday-ui/src/contextualBoxes/index.js b/packages/component-faraday-ui/src/contextualBoxes/index.js index d8e24b485b0ed222d8380682efb5fd3f90b838fc..54a8bdd97d9e793d6924a90a81da41d0fd0020ba 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/index.js +++ b/packages/component-faraday-ui/src/contextualBoxes/index.js @@ -1,2 +1,3 @@ export { default as AssignHE } from './AssignHE' export { default as ReviewerDetails } from './ReviewerDetails' +export { default as ReviewerReportForm } from './ReviewerReportForm' diff --git a/packages/component-faraday-ui/src/helpers/withFetching.js b/packages/component-faraday-ui/src/helpers/withFetching.js index 753c265e6b62deb9df24574fc9cab31572cd9386..717a5f95b9257e38cae9bdad68d910192a896860 100644 --- a/packages/component-faraday-ui/src/helpers/withFetching.js +++ b/packages/component-faraday-ui/src/helpers/withFetching.js @@ -2,12 +2,12 @@ import { withStateHandlers } from 'recompose' export default withStateHandlers( { - isFetching: false, + isFetchingg: false, fetchingError: '', }, { - setFetching: ({ isFetching }) => value => ({ - isFetching: value, + setFetching: ({ isFetchingg }) => value => ({ + isFetchingg: value, }), toggleFetching: ({ isFetching }) => () => ({ isFetching: !isFetching, diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileList.js b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileList.js index 1e2661c510f0245c54c264b2471cd5c163276120..4f4c2ea781867330911b163f54a2b395a2cfdade 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileList.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileList.js @@ -17,16 +17,18 @@ const ManuscriptFileList = ({ onPreview={previewFile} {...rest} /> + <ManuscriptFileSection - label="SUPPLEMENTARY FILES" - list={supplementary} + label="COVER LETTER" + list={coverLetter} onDownload={downloadFile} onPreview={previewFile} {...rest} /> + <ManuscriptFileSection - label="COVER LETTER" - list={coverLetter} + label="SUPPLEMENTARY FILES" + list={supplementary} onDownload={downloadFile} onPreview={previewFile} {...rest} diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js index 8b0976d54a175c262de3880e92097ee97306adb1..de2087fa2452e671d75cddd5908b8f8deeb87243 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js @@ -5,9 +5,11 @@ const ManuscriptFileSection = ({ list = [], label = '', ...rest }) => ( <Fragment> {!!list.length && ( <Fragment> - <Text labelLine mb={1} mt={1}> - {label} - </Text> + {label && ( + <Text labelLine mb={1} mt={1}> + {label} + </Text> + )} <Row justify="flex-start" mb={1}> {list.map(file => ( <Item diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js index c930744e70e8db6829fb9423a52f78066b133b71..4b1119a2e6aeaa49b7aab7f2d02c89835edb0f1c 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js @@ -74,10 +74,12 @@ module.exports = { // send HE emails when a review is submitted // or when the EiC makes a recommendation after peer review + if ( - (isEditorInChief || recommendationType === 'review') && - hasPeerReview && - (recommendation !== 'publish' || hasEQA) + recommendationType === 'review' || + (isEditorInChief && + hasPeerReview && + (recommendation !== 'publish' || hasEQA)) ) { const handlingEditor = get(collection, 'handlingEditor', {}) const heUser = await UserModel.find(handlingEditor.id) diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index e33c34c50a599b8b990deb66f3b006de864023c7..a33f986d60e6d92505444b1aa0fecaa748a3fdf6 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -13,6 +13,9 @@ import { paddingHelper, } from 'pubsweet-component-faraday-ui' +import ReviewerReportCard from './ReviewReportCard' +import ReviewerReportForm from './ReviewerReportForm' + const eicDecisions = [ { value: 'return-to-handling-editor', label: 'Return to Handling Editor' }, { value: 'publish', label: 'Publish' }, @@ -34,25 +37,31 @@ 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, + submittedOwnRecommendation, }) => ( <Root pb={1}> {!isEmpty(collection) && !isEmpty(fragment) ? ( @@ -84,6 +93,29 @@ const ManuscriptLayout = ({ getSignedUrl={getSignedUrl} /> + {submittedOwnRecommendation && ( + <ReviewerReportCard + getSignedUrl={getSignedUrl} + journal={journal} + report={submittedOwnRecommendation} + token={get(currentUser, 'token')} + /> + )} + + {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 ab2934aa03f294caf6ec2818fb100d527a6d0b30..8c1dccd2742b8d97fb40b80294e6ee0acc192b92 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,12 @@ import { canInviteReviewers, pendingHEInvitation, currentUserIsReviewer, - canMakeRecommendation, parseCollectionDetails, pendingReviewerInvitation, canOverrideTechnicalChecks, + getOwnPendingRecommendation, + getOwnSubmittedRecommendation, + getFragmentReviewerRecommendations, getInvitationsWithReviewersForFragment, } from 'pubsweet-component-faraday-selectors' import { RemoteOpener, handleError } from 'pubsweet-component-faraday-ui' @@ -85,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, @@ -93,9 +103,13 @@ export default compose( state, match.params.version, ), + reviewerRecommendations: getFragmentReviewerRecommendations( + state, + match.params.version, + ), }), { - getSignedUrl, + changeForm, clearCustomError, assignHandlingEditor, createRecommendation, @@ -115,6 +129,7 @@ export default compose( collection, currentUser, pendingHEInvitation, + pendingOwnRecommendation, pendingReviewerInvitation, }, ) => ({ @@ -123,21 +138,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: { @@ -146,6 +157,7 @@ export default compose( }, formValues: { eicDecision: getFormValues('eic-decision')(state), + reviewerReport: getFormValues('reviewerReport')(state), responseToInvitation: getFormValues('answer-invitation')(state), }, invitationsWithReviewers: getInvitationsWithReviewersForFragment( @@ -366,12 +378,23 @@ export default compose( toggleReviewerResponse: toggle, reviewerResponseExpanded: expanded, })), + fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ + toggleReviewerRecommendations: toggle, + reviewerRecommendationExpanded: expanded, + })), + withProps(({ currentUser, submittedOwnRecommendation }) => ({ + getSignedUrl, + shouldReview: + get(currentUser, 'isReviewer', false) && + isUndefined(submittedOwnRecommendation), + })), lifecycle({ componentDidMount() { const { match, history, location, + shouldReview, setEditorInChief, clearCustomError, hasManuscriptFailure, @@ -404,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/ReviewReportCard.js b/packages/component-manuscript/src/components/ReviewReportCard.js index 0ea6fdb252d3fc855f364bbe13547ee3f4be5669..35ca28d568e24848ec5e22eb4a91ba6a8f520e8c 100644 --- a/packages/component-manuscript/src/components/ReviewReportCard.js +++ b/packages/component-manuscript/src/components/ReviewReportCard.js @@ -1,151 +1,20 @@ -import React, { Fragment } from 'react' -import { compose } from 'recompose' -import { get, isEmpty } from 'lodash' -import { th } from '@pubsweet/ui-toolkit' -import { withJournal } from 'xpub-journal' -import styled, { css } from 'styled-components' -import { DateParser } from 'pubsweet-components-faraday/src/components' - -import { ShowMore } from './' - -const ReviewReportCard = ({ - i = 0, - report = {}, - showBorder = false, - journal: { recommendations }, -}) => { - const hasReviewer = !isEmpty(get(report, 'user')) - const { submittedOn, comments = [], user } = report - const publicComment = comments.find(c => c.public) - const privateComment = comments.find(c => !c.public) - const recommendationLabel = get( - recommendations.find(r => report.recommendation === r.value), - 'label', - ) - - return ( - <Root showBorder={showBorder}> - {hasReviewer && ( - <Row> - <Text> - <b>Reviewer {i}</b> - <Underline>{user.name}</Underline> - <span>{user.email}</span> - </Text> - <DateParser timestamp={submittedOn}> - {timestamp => <Text>{timestamp}</Text>} - </DateParser> - </Row> - )} - <Row> - <Label>Recommendation</Label> - {!hasReviewer && ( - <DateParser timestamp={submittedOn}> - {timestamp => <Text>{timestamp}</Text>} - </DateParser> - )} - </Row> - <Row> - <Text>{recommendationLabel}</Text> - </Row> - {get(publicComment, 'content') && ( - <Fragment> - <Spacing /> - <Row left> - <Label>Report Text</Label> - </Row> - <Row> - <ShowMore - content={publicComment.content} - id={`public-content-${i}`} - /> - </Row> - </Fragment> - )} - - {get(publicComment, 'files') && - !!publicComment.files.length && ( - <Fragment> - <Spacing /> - <Row left> - <Label>Files</Label> - </Row> - </Fragment> - )} - - {get(privateComment, 'content') && ( - <Fragment> - <Spacing /> - <Row left> - <Label>Confidential Note</Label> - </Row> - <Row> - <ShowMore - content={privateComment.content} - id={`private-content-${i}`} - /> - </Row> - </Fragment> - )} - </Root> - ) -} - -export default compose(withJournal)(ReviewReportCard) - -// #region styled-components -const defaultText = css` - color: ${th('colorPrimary')}; - font-family: ${th('fontReading')}; - font-size: ${th('fontSizeBaseSmall')}; -` - -const cardStyle = css` - margin: 0 auto calc(${th('subGridUnit')}*3); - border: ${th('borderDefault')}; - padding: calc(${th('subGridUnit')}*2); -` - -const Root = styled.div` - display: flex; - flex-direction: column; - margin: auto; - border: none; - padding: 0; - ${({ showBorder }) => (showBorder ? cardStyle : null)}; -` -const Text = styled.div` - ${defaultText}; - span { - margin-left: calc(${th('subGridUnit')}*3); - } -` -const Underline = styled.span` - text-decoration: underline; -` -const Label = styled.div` - ${defaultText}; - text-transform: uppercase; - i { - text-transform: none; - margin-left: ${th('gridUnit')}; - } -` - -const Spacing = styled.div` - margin-top: ${th('gridUnit')}; - 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: ${({ left }) => (left ? 'left' : 'space-between')}; - ${defaultText}; -` - -// #endregion +import React from 'react' +import { + ReviewerReport, + ContextualBox, + withFilePreview, + withFileDownload, +} from 'pubsweet-component-faraday-ui' + +const ReviewReportCard = ({ journal, report, previewFile, downloadFile }) => ( + <ContextualBox label="Your report" mb={2} startExpanded> + <ReviewerReport + journal={journal} + onDownload={downloadFile} + onPreview={previewFile} + report={report} + /> + </ContextualBox> +) + +export default withFileDownload(withFilePreview(ReviewReportCard)) diff --git a/packages/component-manuscript/src/components/ReviewReportCard.old.js b/packages/component-manuscript/src/components/ReviewReportCard.old.js new file mode 100644 index 0000000000000000000000000000000000000000..3262f640303f162ceed9a07a211122b248d21c0a --- /dev/null +++ b/packages/component-manuscript/src/components/ReviewReportCard.old.js @@ -0,0 +1,151 @@ +import React, { Fragment } from 'react' +import { compose } from 'recompose' +import { get, isEmpty } from 'lodash' +import { th } from '@pubsweet/ui-toolkit' +import { withJournal } from 'xpub-journal' +import styled, { css } from 'styled-components' +import { DateParser } from 'pubsweet-components-faraday/src/components' + +import { ShowMore } from '.' + +const ReviewReportCard = ({ + i = 0, + report = {}, + showBorder = false, + journal: { recommendations }, +}) => { + const hasReviewer = !isEmpty(get(report, 'user')) + const { submittedOn, comments = [], user } = report + const publicComment = comments.find(c => c.public) + const privateComment = comments.find(c => !c.public) + const recommendationLabel = get( + recommendations.find(r => report.recommendation === r.value), + 'label', + ) + + return ( + <Root showBorder={showBorder}> + {hasReviewer && ( + <Row> + <Text> + <b>Reviewer {i}</b> + <Underline>{user.name}</Underline> + <span>{user.email}</span> + </Text> + <DateParser timestamp={submittedOn}> + {timestamp => <Text>{timestamp}</Text>} + </DateParser> + </Row> + )} + <Row> + <Label>Recommendation</Label> + {!hasReviewer && ( + <DateParser timestamp={submittedOn}> + {timestamp => <Text>{timestamp}</Text>} + </DateParser> + )} + </Row> + <Row> + <Text>{recommendationLabel}</Text> + </Row> + {get(publicComment, 'content') && ( + <Fragment> + <Spacing /> + <Row left> + <Label>Report Text</Label> + </Row> + <Row> + <ShowMore + content={publicComment.content} + id={`public-content-${i}`} + /> + </Row> + </Fragment> + )} + + {get(publicComment, 'files') && + !!publicComment.files.length && ( + <Fragment> + <Spacing /> + <Row left> + <Label>Files</Label> + </Row> + </Fragment> + )} + + {get(privateComment, 'content') && ( + <Fragment> + <Spacing /> + <Row left> + <Label>Confidential Note</Label> + </Row> + <Row> + <ShowMore + content={privateComment.content} + id={`private-content-${i}`} + /> + </Row> + </Fragment> + )} + </Root> + ) +} + +export default compose(withJournal)(ReviewReportCard) + +// #region styled-components +const defaultText = css` + color: ${th('colorPrimary')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` + +const cardStyle = css` + margin: 0 auto calc(${th('subGridUnit')}*3); + border: ${th('borderDefault')}; + padding: calc(${th('subGridUnit')}*2); +` + +const Root = styled.div` + display: flex; + flex-direction: column; + margin: auto; + border: none; + padding: 0; + ${({ showBorder }) => (showBorder ? cardStyle : null)}; +` +const Text = styled.div` + ${defaultText}; + span { + margin-left: calc(${th('subGridUnit')}*3); + } +` +const Underline = styled.span` + text-decoration: underline; +` +const Label = styled.div` + ${defaultText}; + text-transform: uppercase; + i { + text-transform: none; + margin-left: ${th('gridUnit')}; + } +` + +const Spacing = styled.div` + margin-top: ${th('gridUnit')}; + 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: ${({ left }) => (left ? 'left' : 'space-between')}; + ${defaultText}; +` + +// #endregion diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.js b/packages/component-manuscript/src/components/ReviewerReportForm.js index d0285b89b38717cabd9eb46dd0206d985d5775ae..cf886b32a4f4b10bce0de9796e98f3a2ba3da049 100644 --- a/packages/component-manuscript/src/components/ReviewerReportForm.js +++ b/packages/component-manuscript/src/components/ReviewerReportForm.js @@ -1,197 +1,84 @@ -import React, { Fragment } from 'react' -import { connect } from 'react-redux' -import { isEmpty, merge } from 'lodash' -import { th } from '@pubsweet/ui-toolkit' +import { get } from 'lodash' +import { reduxForm } from 'redux-form' import { withJournal } from 'xpub-journal' -import { required } from 'xpub-validators' -import styled, { css } from 'styled-components' -import { compose, withHandlers, withProps } from 'recompose' -import { Menu, Icon, Button, ErrorText, ValidatedField } from '@pubsweet/ui' +import { withModal } from 'pubsweet-component-modal/src/components' +import { compose, withHandlers, withProps, withState } from 'recompose' + import { - reduxForm, - isSubmitting, - getFormValues, - change as changeForm, -} from 'redux-form' + MultiAction, + ReviewerReportForm, + handleError, + withFetching, + withFilePreview, + withFileDownload, +} from 'pubsweet-component-faraday-ui' import { uploadFile, deleteFile, getSignedUrl, - getRequestStatus, } from 'pubsweet-components-faraday/src/redux/files' -import { - withModal, - ConfirmationModal, -} from 'pubsweet-component-modal/src/components' - -import { - createRecommendation, - updateRecommendation, -} from 'pubsweet-components-faraday/src/redux/recommendations' - import { onReviewSubmit, onReviewChange, + reviewerReportValidate, parseReviewResponseToForm, } from './utils' -const guidelinesLink = - 'https://about.hindawi.com/authors/peer-review-at-hindawi/' - -const TextAreaField = input => <Textarea {...input} height={70} rows={6} /> - -const ReviewerReportForm = ({ - addFile, - fileError, - removeFile, - changeField, - isSubmitting, - handleSubmit, - fileFetching, - review = {}, - formValues = {}, - journal: { recommendations }, -}) => ( - <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={recommendations} - placeholder="Please select" - /> - )} - name="recommendation" - validate={[required]} - /> - </Row> - - <Spacing /> - - <Row> - <FullWidth className="full-width"> - <ValidatedField - component={TextAreaField} - name="public" - validate={isEmpty(formValues.files) ? [required] : []} - /> - </FullWidth> - </Row> - - {formValues.hasConfidential ? ( - <Fragment> - <Spacing /> - <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={TextAreaField} - name="confidential" - validate={[required]} - /> - </FullWidth> - </Row> - </Fragment> - ) : ( - <Row> - <ActionText onClick={() => changeField('hasConfidential', true)}> - Add confidential note for the Editorial Team - </ActionText> - </Row> - )} - - <Spacing /> - {fileError && ( - <Row> - <ErrorText>{fileError}</ErrorText> - </Row> - )} - <Row> - <ActionButton onClick={handleSubmit}>Submit report</ActionButton> - </Row> - </Root> -) - -const ModalWrapper = compose( - connect(state => ({ - fetching: false, - })), -)(({ fetching, ...rest }) => ( - <ConfirmationModal {...rest} isFetching={fetching} /> -)) - +// #region export export default compose( withJournal, - connect( - state => ({ - fileFetching: getRequestStatus(state), - formValues: getFormValues('reviewerReport')(state), - isSubmitting: isSubmitting('reviewerReport')(state), - }), - { - uploadFile, - deleteFile, - changeForm, - getSignedUrl, - getFormValues, - createRecommendation, - updateRecommendation, - }, - ), + withFetching, withProps(({ review = {}, formValues = {} }) => ({ - initialValues: merge(parseReviewResponseToForm(review), formValues), + getSignedUrl, + initialValues: parseReviewResponseToForm(review), })), - withModal(props => ({ - modalComponent: ModalWrapper, + withFilePreview, + withFileDownload, + withModal(({ isFetching, modalKey }) => ({ + modalKey, + isFetching, + modalComponent: MultiAction, })), + withState( + 'hasNote', + 'setNote', + ({ review }) => get(review, 'comments', []).length === 2, + ), withHandlers({ - changeField: ({ changeForm }) => (field, value) => { - changeForm('reviewerReport', field, value) + addNote: ({ setNote }) => () => { + setNote(true) }, - addFile: ({ formValues = {}, uploadFile, changeForm, version }) => file => { - uploadFile(file, 'review', version) + removeNote: ({ setNote, changeForm }) => () => { + changeForm('reviewerReport', 'confidential', '') + setNote(false) + }, + addFile: ({ version, changeForm, setFetching, setError }) => file => { + setFetching(true) + setError('') + uploadFile({ file, type: 'review', fragment: version }) .then(file => { - const files = formValues.files || [] - const newFiles = [...files, file] - changeForm('reviewerReport', 'files', newFiles) + setFetching(false) + changeForm('reviewerReport', 'file', file) + }) + .catch(err => { + setFetching(false) + handleError(setError)(err) }) - .catch(e => console.error(`Couldn't upload file.`, e)) }, - removeFile: ({ - formValues: { files = [] }, - changeForm, - deleteFile, - }) => id => e => { - deleteFile(id) + removeFile: ({ changeForm, setError, setFetching }) => file => { + setFetching(true) + setError('') + deleteFile(file.id) .then(r => { - const newFiles = files.filter(f => f.id !== id) - changeForm('reviewerReport', 'files', newFiles) + setFetching(false) + changeForm('reviewerReport', 'file', null) + }) + .catch(err => { + setFetching(false) + handleError(setError)(err) }) - .catch(e => console.error(`Couldn't delete the file.`, e)) - - const newFiles = files.filter(f => f.id !== id) - changeForm('reviewerReport', 'files', newFiles) }, }), reduxForm({ @@ -200,99 +87,7 @@ export default compose( onSubmit: onReviewSubmit, enableReinitialize: false, keepDirtyOnReinitialize: true, + validate: reviewerReportValidate, }), )(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}; - cursor: pointer; - margin-left: ${({ left }) => left || 0}px; - text-decoration: underline; -` - -const ActionTextIcon = styled(ActionText)` - align-items: center; - display: flex; -` -const ActionLink = styled.a` - ${defaultText}; -` - -const Textarea = styled.textarea` - border-color: ${({ hasError }) => - hasError ? th('colorError') : th('colorPrimary')}; - font-size: ${th('fontSizeBaseSmall')}; - font-family: ${th('fontWriting')}; - padding: calc(${th('subGridUnit')} * 2); - transition: all 300ms linear; - width: 100%; - - &:read-only { - background-color: ${th('colorBackgroundHue')}; - } -` - -const Spacing = styled.div` - flex: 1; - margin-top: calc(${th('gridUnit')} / 2); -` - -const FullWidth = styled.div` - flex: 1; - > div { - flex: 1; - } -` - -const Row = styled.div` - ${defaultText}; - align-items: center; - box-sizing: border-box; - display: flex; - flex-direction: row; - flex: 1; - flex-wrap: wrap; - justify-content: ${({ left }) => (left ? 'left' : 'space-between')}; - - div[role='alert'] { - margin-top: 0; - } -` - -const ActionButton = styled(Button)` - ${defaultText}; - align-items: center; - background-color: ${th('colorPrimary')}; - color: ${th('colorTextReverse')}; - height: calc(${th('subGridUnit')} * 5); - display: flex; - padding: calc(${th('subGridUnit')} / 2) calc(${th('subGridUnit')}); - text-align: center; - text-transform: uppercase; -` // #endregion diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.old.js b/packages/component-manuscript/src/components/ReviewerReportForm.old.js new file mode 100644 index 0000000000000000000000000000000000000000..d0285b89b38717cabd9eb46dd0206d985d5775ae --- /dev/null +++ b/packages/component-manuscript/src/components/ReviewerReportForm.old.js @@ -0,0 +1,298 @@ +import React, { Fragment } from 'react' +import { connect } from 'react-redux' +import { isEmpty, merge } from 'lodash' +import { th } from '@pubsweet/ui-toolkit' +import { withJournal } from 'xpub-journal' +import { required } from 'xpub-validators' +import styled, { css } from 'styled-components' +import { compose, withHandlers, withProps } from 'recompose' +import { Menu, Icon, Button, ErrorText, ValidatedField } from '@pubsweet/ui' +import { + reduxForm, + isSubmitting, + getFormValues, + change as changeForm, +} from 'redux-form' + +import { + uploadFile, + deleteFile, + getSignedUrl, + getRequestStatus, +} from 'pubsweet-components-faraday/src/redux/files' + +import { + withModal, + ConfirmationModal, +} from 'pubsweet-component-modal/src/components' + +import { + createRecommendation, + updateRecommendation, +} from 'pubsweet-components-faraday/src/redux/recommendations' + +import { + onReviewSubmit, + onReviewChange, + parseReviewResponseToForm, +} from './utils' + +const guidelinesLink = + 'https://about.hindawi.com/authors/peer-review-at-hindawi/' + +const TextAreaField = input => <Textarea {...input} height={70} rows={6} /> + +const ReviewerReportForm = ({ + addFile, + fileError, + removeFile, + changeField, + isSubmitting, + handleSubmit, + fileFetching, + review = {}, + formValues = {}, + journal: { recommendations }, +}) => ( + <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={recommendations} + placeholder="Please select" + /> + )} + name="recommendation" + validate={[required]} + /> + </Row> + + <Spacing /> + + <Row> + <FullWidth className="full-width"> + <ValidatedField + component={TextAreaField} + name="public" + validate={isEmpty(formValues.files) ? [required] : []} + /> + </FullWidth> + </Row> + + {formValues.hasConfidential ? ( + <Fragment> + <Spacing /> + <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={TextAreaField} + name="confidential" + validate={[required]} + /> + </FullWidth> + </Row> + </Fragment> + ) : ( + <Row> + <ActionText onClick={() => changeField('hasConfidential', true)}> + Add confidential note for the Editorial Team + </ActionText> + </Row> + )} + + <Spacing /> + {fileError && ( + <Row> + <ErrorText>{fileError}</ErrorText> + </Row> + )} + <Row> + <ActionButton onClick={handleSubmit}>Submit report</ActionButton> + </Row> + </Root> +) + +const ModalWrapper = compose( + connect(state => ({ + fetching: false, + })), +)(({ fetching, ...rest }) => ( + <ConfirmationModal {...rest} isFetching={fetching} /> +)) + +export default compose( + withJournal, + connect( + state => ({ + fileFetching: getRequestStatus(state), + formValues: getFormValues('reviewerReport')(state), + isSubmitting: isSubmitting('reviewerReport')(state), + }), + { + uploadFile, + deleteFile, + changeForm, + getSignedUrl, + getFormValues, + createRecommendation, + updateRecommendation, + }, + ), + withProps(({ review = {}, formValues = {} }) => ({ + initialValues: merge(parseReviewResponseToForm(review), formValues), + })), + withModal(props => ({ + modalComponent: ModalWrapper, + })), + withHandlers({ + changeField: ({ changeForm }) => (field, value) => { + changeForm('reviewerReport', field, value) + }, + addFile: ({ formValues = {}, uploadFile, changeForm, version }) => file => { + uploadFile(file, 'review', version) + .then(file => { + const files = formValues.files || [] + const newFiles = [...files, file] + changeForm('reviewerReport', 'files', newFiles) + }) + .catch(e => console.error(`Couldn't upload file.`, e)) + }, + removeFile: ({ + formValues: { files = [] }, + changeForm, + deleteFile, + }) => id => e => { + deleteFile(id) + .then(r => { + const newFiles = files.filter(f => f.id !== id) + changeForm('reviewerReport', 'files', newFiles) + }) + .catch(e => console.error(`Couldn't delete the file.`, e)) + + const newFiles = files.filter(f => f.id !== id) + changeForm('reviewerReport', 'files', newFiles) + }, + }), + reduxForm({ + form: 'reviewerReport', + onChange: onReviewChange, + onSubmit: onReviewSubmit, + enableReinitialize: false, + keepDirtyOnReinitialize: true, + }), +)(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}; + cursor: pointer; + margin-left: ${({ left }) => left || 0}px; + text-decoration: underline; +` + +const ActionTextIcon = styled(ActionText)` + align-items: center; + display: flex; +` +const ActionLink = styled.a` + ${defaultText}; +` + +const Textarea = styled.textarea` + border-color: ${({ hasError }) => + hasError ? th('colorError') : th('colorPrimary')}; + font-size: ${th('fontSizeBaseSmall')}; + font-family: ${th('fontWriting')}; + padding: calc(${th('subGridUnit')} * 2); + transition: all 300ms linear; + width: 100%; + + &:read-only { + background-color: ${th('colorBackgroundHue')}; + } +` + +const Spacing = styled.div` + flex: 1; + margin-top: calc(${th('gridUnit')} / 2); +` + +const FullWidth = styled.div` + flex: 1; + > div { + flex: 1; + } +` + +const Row = styled.div` + ${defaultText}; + align-items: center; + box-sizing: border-box; + display: flex; + flex-direction: row; + flex: 1; + flex-wrap: wrap; + justify-content: ${({ left }) => (left ? 'left' : 'space-between')}; + + div[role='alert'] { + margin-top: 0; + } +` + +const ActionButton = styled(Button)` + ${defaultText}; + align-items: center; + background-color: ${th('colorPrimary')}; + color: ${th('colorTextReverse')}; + height: calc(${th('subGridUnit')} * 5); + display: flex; + padding: calc(${th('subGridUnit')} / 2) calc(${th('subGridUnit')}); + text-align: center; + text-transform: uppercase; +` +// #endregion diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js index 290712b167b1f6f1ca6dcc110227a39c41df5d21..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,15 +9,22 @@ import { mergeWith, capitalize, } from 'lodash' +import { change as changeForm } from 'redux-form' import { actions } from 'pubsweet-client/src' -import { change as changeForm } from 'redux-form' +import { handleError } from 'pubsweet-component-faraday-ui' + import { autosaveRequest, - autosaveSuccess, autosaveFailure, + autosaveSuccess, } from 'pubsweet-component-wizard/src/redux/autosave' +import { + createRecommendation, + updateRecommendation, +} from '../redux/recommendations' + export const parseTitle = version => { const title = get(version, 'metadata.title') if (title) { @@ -99,15 +107,14 @@ export const redirectToError = redirectFn => err => { export const parseReviewResponseToForm = (review = {}) => { if (isEmpty(review)) return {} - const comments = review.comments || [] + const comments = get(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'), + file: get(publicComment, 'files.0', null), confidential: get(privateComment, 'content'), - hasConfidential: !!get(privateComment, 'content'), } } @@ -116,52 +123,71 @@ export const parseReviewRequest = (review = {}) => { const comments = [ { public: true, - content: review.public || undefined, - files: review.files || [], + files: has(review, 'file') ? [get(review, 'file')] : [], + content: get(review, 'public'), }, ] - if (review.hasConfidential) { + if (get(review, 'confidential', '')) { comments.push({ public: false, - content: review.confidential || undefined, + content: get(review, 'confidential'), files: [], }) } return { - ...omit(review, [ - 'public', - 'confidential', - 'hasConfidential', - 'files', - 'userId', - ]), - recommendationType: 'review', + id: get(review, 'id', null), comments, + recommendationType: 'review', + recommendation: get(review, 'recommendation', 'publish'), } } -const onChange = ( - values, - dispatch, - { project, version, createRecommendation, updateRecommendation }, -) => { +export const reviewerReportValidate = values => { + const errors = {} + + if (!values.public && !values.file) { + errors.public = 'A file or a public report is required.' + } + + return errors +} + +const onChange = (values, dispatch, { project, version }) => { const newValues = parseReviewRequest(values) - // if (!isEqual(newValues, prevValues)) { dispatch(autosaveRequest()) if (newValues.id) { - updateRecommendation(project.id, version.id, newValues) - .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn')))) - .catch(e => dispatch(autosaveFailure(e))) + updateRecommendation({ + fragmentId: version.id, + collectionId: project.id, + recommendation: newValues, + }).then( + r => { + dispatch(autosaveSuccess(Date.now())) + return r + }, + err => { + dispatch(autosaveFailure()) + throw err + }, + ) } else { - createRecommendation(project.id, version.id, newValues) - .then(r => { + createRecommendation({ + fragmentId: version.id, + collectionId: project.id, + recommendation: omit(newValues, 'id'), + }).then( + r => { dispatch(changeForm('reviewerReport', 'id', r.id)) - return dispatch(autosaveSuccess(get(r, 'updatedOn'))) - }) - .catch(e => dispatch(autosaveFailure(e))) + dispatch(autosaveSuccess(Date.now())) + return r + }, + err => { + dispatch(autosaveFailure()) + throw err + }, + ) } - // } } export const onReviewChange = debounce(onChange, 1000, { maxWait: 5000 }) @@ -169,31 +195,30 @@ export const onReviewChange = debounce(onChange, 1000, { maxWait: 5000 }) export const onReviewSubmit = ( values, dispatch, - { - project, - version, - showModal, - hideModal, - isSubmitting, - updateRecommendation, - }, + { project, version, showModal, setFetching, isSubmitting }, ) => { showModal({ title: 'Ready to Submit your Report?', subtitle: 'Once submitted, the report can`t be modified', confirmText: 'Submit report', - onConfirm: () => { + onConfirm: ({ hideModal, setModalError }) => { + setFetching(true) const newValues = parseReviewRequest(values) newValues.submittedOn = Date.now() - dispatch(autosaveRequest()) - updateRecommendation(project.id, version.id, newValues) - .then(r => dispatch(autosaveSuccess(get(r, 'updatedOn')))) + updateRecommendation({ + fragmentId: version.id, + collectionId: project.id, + recommendation: newValues, + }) .then(() => { dispatch(actions.getFragments({ id: project.id })) hideModal() }) + .catch(err => { + setFetching(false) + handleError(setModalError)(err) + }) }, - onCancel: hideModal, }) } diff --git a/packages/component-manuscript/src/index.js b/packages/component-manuscript/src/index.js index 9e0071c956db532671fb8e5e7b01b5a936ab193d..01aa93785c2d01ec0fb22a177c2c6a123b2e71b0 100644 --- a/packages/component-manuscript/src/index.js +++ b/packages/component-manuscript/src/index.js @@ -3,7 +3,6 @@ module.exports = { components: [() => require('./components')], reducers: { editors: () => require('./redux/editors').default, - recommendations: () => require('./redux/recommendations').default, }, }, } diff --git a/packages/component-manuscript/src/redux/index.js b/packages/component-manuscript/src/redux/index.js deleted file mode 100644 index 05f36f6d43441bbc101f5490a0aa3677ecf26a15..0000000000000000000000000000000000000000 --- a/packages/component-manuscript/src/redux/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as editors } from './editors' diff --git a/packages/component-manuscript/src/redux/recommendations.js b/packages/component-manuscript/src/redux/recommendations.js index 9b530a3169845e974339dbf861236b00db09d9a3..154e043de88b4100daca4a96da85243fa26ebb20 100644 --- a/packages/component-manuscript/src/redux/recommendations.js +++ b/packages/component-manuscript/src/redux/recommendations.js @@ -1,17 +1,6 @@ import { get } from 'lodash' import { create, update } from 'pubsweet-client/src/helpers/api' -const RECOMMENDATION_REQUEST = 'recommendation/REQUEST' -const RECOMMENDATION_DONE = 'recommendation/DONE' - -const recommendationRequest = () => ({ - type: RECOMMENDATION_REQUEST, -}) - -const recommendationDone = () => ({ - type: RECOMMENDATION_DONE, -}) - // #region Selectors export const selectRecommendations = (state, fragmentId) => get(state, `fragments.${fragmentId}.recommendations`, []) @@ -34,45 +23,21 @@ export const createRecommendation = ({ fragmentId, collectionId, recommendation, -}) => dispatch => { - dispatch(recommendationRequest()) - return create( +}) => + create( `/collections/${collectionId}/fragments/${fragmentId}/recommendations`, recommendation, - ).then( - res => { - dispatch(recommendationDone()) - return res - }, - err => { - dispatch(recommendationDone()) - throw err - }, ) -} -export const updateRecommendation = ( - collId, - fragId, +export const updateRecommendation = ({ + fragmentId, + collectionId, recommendation, -) => dispatch => { - dispatch(recommendationRequest()) - return update( - `/collections/${collId}/fragments/${fragId}/recommendations/${ +}) => + update( + `/collections/${collectionId}/fragments/${fragmentId}/recommendations/${ recommendation.id }`, recommendation, - ).then( - res => { - dispatch(recommendationDone()) - return res - }, - err => { - dispatch(recommendationDone()) - throw err - }, ) -} // #endregion - -export default (state = false, action) => action.type === RECOMMENDATION_REQUEST diff --git a/packages/component-modal/src/components/withModal.js b/packages/component-modal/src/components/withModal.js index 2be2e0d14d6a1cea34baffbab57d5d51cbb5df9b..3d7f9913d86dd3fb67526c1b0e81d958c7df2a76 100644 --- a/packages/component-modal/src/components/withModal.js +++ b/packages/component-modal/src/components/withModal.js @@ -49,6 +49,7 @@ const withModal = mapperFn => BaseComponent => )} <BaseComponent hideModal={hideModal} + isFetching={isFetching} setModalError={setModalError} showModal={showModal} {...rest} diff --git a/packages/component-wizard/src/components/SubmissionWizard.js b/packages/component-wizard/src/components/SubmissionWizard.js index 0d1a19334f9c359ba17cb1a0488f7b0b5a767a6a..74047fae170ce629995ecc945d4acecb70fc8e51 100644 --- a/packages/component-wizard/src/components/SubmissionWizard.js +++ b/packages/component-wizard/src/components/SubmissionWizard.js @@ -128,9 +128,6 @@ export default compose( { addAuthor, changeForm, - uploadFile, - deleteFile, - getSignedUrl, deleteAuthor, submitManuscript, updateFragment: actions.updateFragment, @@ -159,6 +156,9 @@ export default compose( authorEditIndex, reduxAuthorError, }) => ({ + deleteFile, + uploadFile, + getSignedUrl, isFirstStep: step === 0, isAuthorEdit: !isNull(authorEditIndex), isLastStep: step === wizardSteps.length - 1, diff --git a/packages/components-faraday/src/redux/files.js b/packages/components-faraday/src/redux/files.js index 623177c8a4284d3641896c94c86ba1157805494e..02a36e8517af01e2768c6868b75c67467d78e557 100644 --- a/packages/components-faraday/src/redux/files.js +++ b/packages/components-faraday/src/redux/files.js @@ -19,20 +19,6 @@ const REMOVE_FAILURE = 'files/REMOVE_FAILURE' const REMOVE_SUCCESS = 'files/REMOVE_SUCCESS' // action creators -const uploadRequest = type => ({ - type: UPLOAD_REQUEST, - fileType: type, -}) - -const uploadFailure = error => ({ - type: UPLOAD_FAILURE, - error, -}) - -const uploadSuccess = () => ({ - type: UPLOAD_SUCCESS, -}) - const createFileData = ({ file, type, fragmentId, newName }) => { const data = new FormData() data.append('fileType', type) @@ -49,7 +35,7 @@ const createFileData = ({ file, type, fragmentId, newName }) => { } } -const setFileName = (file, { files }) => { +const setFileName = (file, { files = [] }) => { let newFilename = file.name const fileCount = Object.values(files) .reduce((acc, f) => [...acc, ...f], []) @@ -69,20 +55,6 @@ const setFileName = (file, { files }) => { return newFilename } -const removeRequest = () => ({ - type: REMOVE_REQUEST, -}) - -const removeFailure = error => ({ - type: REMOVE_FAILURE, - error, -}) - -const removeSuccess = file => ({ - type: REMOVE_SUCCESS, - file, -}) - // selectors export const getRequestStatus = state => get(state, 'files.isFetching', false) export const getFileFetching = state => @@ -92,38 +64,18 @@ export const getFileFetching = state => export const getFileError = state => get(state, 'files.error', null) // thunked actions -export const uploadFile = (file, type, fragment) => dispatch => { - dispatch(uploadRequest(type)) +export const uploadFile = ({ file, type, fragment }) => { const newName = setFileName(file, fragment) return request( '/files', createFileData({ file, type, fragmentId: fragment.id, newName }), - ).then( - r => { - dispatch(uploadSuccess()) - return r - }, - error => { - dispatch(uploadFailure(error)) - throw error - }, ) } -export const deleteFile = (fileId, type = 'manuscripts') => dispatch => { - dispatch(removeRequest(type)) - return remove(`/files/${fileId}`) - .then(r => { - dispatch(removeSuccess()) - return r - }) - .catch(err => { - dispatch(removeFailure(err.message)) - throw err - }) -} +export const deleteFile = (fileId, type = 'manuscripts') => + remove(`/files/${fileId}`) -export const getSignedUrl = fileId => dispatch => apiGet(`/files/${fileId}`) +export const getSignedUrl = fileId => apiGet(`/files/${fileId}`) // reducer export default (state = initialState, action) => { diff --git a/packages/components-faraday/src/redux/recommendations.js b/packages/components-faraday/src/redux/recommendations.js index 49f8ecc81bd0fe5ea39cf24a4d526f96a48e59a8..fb3d21fef4d517c04bfc5e811db9a13ef1965150 100644 --- a/packages/components-faraday/src/redux/recommendations.js +++ b/packages/components-faraday/src/redux/recommendations.js @@ -16,23 +16,23 @@ export const selectReviewRecommendations = (state, fragmentId) => // #region Actions // error handling and fetching is handled by the autosave reducer -export const createRecommendation = ( - collId, - fragId, +export const createRecommendation = ({ + fragmentId, + collectionId, recommendation, -) => dispatch => +}) => create( - `/collections/${collId}/fragments/${fragId}/recommendations`, + `/collections/${collectionId}/fragments/${fragmentId}/recommendations`, recommendation, ) -export const updateRecommendation = ( - collId, - fragId, +export const updateRecommendation = ({ + fragmentId, + collectionId, recommendation, -) => dispatch => +}) => update( - `/collections/${collId}/fragments/${fragId}/recommendations/${ + `/collections/${collectionId}/fragments/${fragmentId}/recommendations/${ recommendation.id }`, recommendation,