From fe0b497334bae8f777edc9aaea963e72bd61f3db Mon Sep 17 00:00:00 2001 From: Alexandru Munteanu <alexandru.munt@gmail.com> Date: Thu, 27 Sep 2018 14:43:05 +0300 Subject: [PATCH] feat(reviewer-report): reviewer report form --- .../component-faraday-selectors/src/index.js | 40 +- packages/component-faraday-ui/src/File.js | 3 + packages/component-faraday-ui/src/Textarea.js | 4 - .../src/contextualBoxes/ReviewerReportForm.js | 174 ++++++++ .../src/contextualBoxes/ReviewerReportForm.md | 10 + .../src/contextualBoxes/index.js | 1 + .../ManuscriptFileSection.js | 8 +- .../src/components/ManuscriptLayout.js | 8 + .../src/components/ManuscriptPage.js | 5 + .../src/components/ReviewerReportForm.js | 391 ++++++++---------- .../src/components/ReviewerReportForm.old.js | 298 +++++++++++++ .../src/components/utils.js | 112 +++-- packages/component-manuscript/src/index.js | 1 - .../component-manuscript/src/redux/index.js | 1 - .../src/redux/recommendations.js | 51 +-- .../src/components/withModal.js | 1 + .../components-faraday/src/redux/files.js | 58 +-- .../src/redux/recommendations.js | 20 +- 18 files changed, 789 insertions(+), 397 deletions(-) create mode 100644 packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js create mode 100644 packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md create mode 100644 packages/component-manuscript/src/components/ReviewerReportForm.old.js delete mode 100644 packages/component-manuscript/src/redux/index.js diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index 6c9ebc928..46e0a68e5 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -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,31 @@ 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`, []) + +export const getFragmentReviewerRecommendations = (state, fragmentId) => + getFragmentRecommendations(state, fragmentId).filter( + r => r.recommendationType === 'review', + ) + +export const getOwnRecommendation = (state, fragmentId) => + chain(state) + .get(`fragments.${fragmentId}.recommendations`, []) + .find(r => r.userId === get(state, 'currentUser.user.id', '')) + .value() +// #endregion diff --git a/packages/component-faraday-ui/src/File.js b/packages/component-faraday-ui/src/File.js index 99a4afea1..4c6014d3e 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/Textarea.js b/packages/component-faraday-ui/src/Textarea.js index bd385619a..122cf5a68 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/contextualBoxes/ReviewerReportForm.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js new file mode 100644 index 000000000..3090f3363 --- /dev/null +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.js @@ -0,0 +1,174 @@ +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 { + Row, + 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, + addFile, + expanded, + toggleMenu, + handleSubmit, +}) => ( + <ContextualBox label="Your report" startExpanded> + <Root> + <Row> + <ItemOverrideAlert vertical> + <Label required>Recommendation</Label> + <ValidatedField + component={input => <Menu {...input} options={options} />} + name="recommendation" + validate={[required]} + /> + </ItemOverrideAlert> + </Row> + + <Row alignItems="center" justify="space-between" mt={1}> + <Item> + <Label required>Your report</Label> + <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}> + <ItemOverrideAlert vertical> + <ValidatedField + component={Textarea} + name="message" + validate={[required]} + /> + </ItemOverrideAlert> + </Row> + + <Row justify="flex-start"> + <Item flex={0}> + <FileItem item={file} /> + </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={toggleMenu}> + Remove + </ActionLink> + </Item> + </Fragment> + ) : ( + <Item> + <ActionLink onClick={toggleMenu}> + Add Confidential note for the Editorial Team + </ActionLink> + </Item> + )} + </Row> + + {expanded && ( + <Row> + <ItemOverrideAlert pr={2} vertical> + <ValidatedField component={Textarea} name="note" /> + </ItemOverrideAlert> + </Row> + )} + + <Row justify="flex-end" mt={2}> + <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) + +// #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/ReviewerReportForm.md b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md new file mode 100644 index 000000000..bf3998da8 --- /dev/null +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerReportForm.md @@ -0,0 +1,10 @@ +Reviewer report contextual box. + +```js +<ReviewerReportForm + onSubmitReport={(values, { setFetching }) => { + console.log('submitting report', values) + setFetching(true) + }} +/> +``` diff --git a/packages/component-faraday-ui/src/contextualBoxes/index.js b/packages/component-faraday-ui/src/contextualBoxes/index.js index d8e24b485..54a8bdd97 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/manuscriptDetails/ManuscriptFileSection.js b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptFileSection.js index 8b0976d54..de2087fa2 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/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index e33c34c50..0099f2215 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -13,6 +13,8 @@ import { paddingHelper, } from 'pubsweet-component-faraday-ui' +import ReviewerReportForm from './ReviewerReportForm' + const eicDecisions = [ { value: 'return-to-handling-editor', label: 'Return to Handling Editor' }, { value: 'publish', label: 'Publish' }, @@ -78,6 +80,12 @@ const ManuscriptLayout = ({ revokeInvitation={revokeHE} /> + <ReviewerReportForm + modalKey="reviewer-report" + project={collection} + version={fragment} + /> + <ManuscriptMetadata currentUser={currentUser} fragment={fragment} diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index ab2934aa0..02c7b2b50 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -44,6 +44,7 @@ import { parseCollectionDetails, pendingReviewerInvitation, canOverrideTechnicalChecks, + getFragmentReviewerRecommendations, getInvitationsWithReviewersForFragment, } from 'pubsweet-component-faraday-selectors' import { RemoteOpener, handleError } from 'pubsweet-component-faraday-ui' @@ -93,6 +94,10 @@ export default compose( state, match.params.version, ), + reviewerRecommendations: getFragmentReviewerRecommendations( + state, + match.params.version, + ), }), { getSignedUrl, diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.js b/packages/component-manuscript/src/components/ReviewerReportForm.js index d0285b89b..64db430c3 100644 --- a/packages/component-manuscript/src/components/ReviewerReportForm.js +++ b/packages/component-manuscript/src/components/ReviewerReportForm.js @@ -1,197 +1,223 @@ import React, { Fragment } from 'react' +import { get } from 'lodash' import { connect } from 'react-redux' -import { isEmpty, merge } from 'lodash' +import styled from 'styled-components' 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 { 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 { - reduxForm, - isSubmitting, - getFormValues, - change as changeForm, -} from 'redux-form' + Row, + Item, + Text, + Label, + FileItem, + Textarea, + ActionLink, + MultiAction, + ContextualBox, + ItemOverrideAlert, + handleError, + withFetching, + withFilePreview, + withFileDownload, +} from 'pubsweet-component-faraday-ui' + +import { getOwnRecommendation } from 'pubsweet-component-faraday-selectors' 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 = ({ + addNote, addFile, + expanded, fileError, + isFetching, + removeNote, removeFile, - changeField, - isSubmitting, + previewFile, + downloadFile, handleSubmit, - fileFetching, + fetchingError, 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" + <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]} /> - )} - name="recommendation" - validate={[required]} - /> - </Row> + </ItemOverrideAlert> + </Row> - <Spacing /> + <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> - <FullWidth className="full-width"> - <ValidatedField - component={TextAreaField} - name="public" - validate={isEmpty(formValues.files) ? [required] : []} - /> - </FullWidth> - </Row> + <Row mb={1 / 2}> + <ItemOverrideAlert vertical> + <ValidatedField component={Textarea} name="public" /> + </ItemOverrideAlert> + </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]} + {formValues.file && ( + <Row justify="flex-start" mb={1}> + <Item flex={0}> + <FileItem + item={formValues.file} + onDelete={removeFile} + onDownload={downloadFile} + onPreview={previewFile} /> - </FullWidth> + </Item> </Row> - </Fragment> - ) : ( - <Row> - <ActionText onClick={() => changeField('hasConfidential', true)}> - Add confidential note for the Editorial Team - </ActionText> + )} + + <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> - )} - <Spacing /> - {fileError && ( - <Row> - <ErrorText>{fileError}</ErrorText> + {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> - )} - <Row> - <ActionButton onClick={handleSubmit}>Submit report</ActionButton> - </Row> - </Root> + </Root> + </ContextualBox> ) -const ModalWrapper = compose( - connect(state => ({ - fetching: false, - })), -)(({ fetching, ...rest }) => ( - <ConfirmationModal {...rest} isFetching={fetching} /> -)) - +// #region export export default compose( withJournal, + withFetching, connect( - state => ({ - fileFetching: getRequestStatus(state), + (state, { version, ...rest }) => ({ + review: getOwnRecommendation(state, version.id), + token: get(state, 'currentUser.user.token', ''), formValues: getFormValues('reviewerReport')(state), - isSubmitting: isSubmitting('reviewerReport')(state), }), { - uploadFile, - deleteFile, changeForm, - getSignedUrl, - getFormValues, - createRecommendation, - updateRecommendation, }, ), 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( + 'expanded', + 'setExpanded', + ({ review }) => get(review, 'comments', []).length === 2, + ), withHandlers({ - changeField: ({ changeForm }) => (field, value) => { - changeForm('reviewerReport', field, value) + addNote: ({ setExpanded }) => () => { + setExpanded(true) + }, + removeNote: ({ setExpanded, changeForm }) => () => { + changeForm('reviewerReport', 'confidential', '') + setExpanded(false) }, - addFile: ({ formValues = {}, uploadFile, changeForm, version }) => file => { - uploadFile(file, 'review', version) + 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 +226,14 @@ export default compose( onSubmit: onReviewSubmit, enableReinitialize: false, keepDirtyOnReinitialize: true, + validate: reviewerReportValidate, }), )(ReviewerReportForm) +// #endregion // #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; + background-color: ${th('colorBackgroundHue2')}; + padding: calc(${th('gridUnit')} * 2); ` // #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 000000000..d0285b89b --- /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 290712b16..28d7fef24 100644 --- a/packages/component-manuscript/src/components/utils.js +++ b/packages/component-manuscript/src/components/utils.js @@ -10,13 +10,19 @@ import { } from 'lodash' 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 +105,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,12 +121,12 @@ export const parseReviewRequest = (review = {}) => { const comments = [ { public: true, - content: review.public || undefined, - files: review.files || [], + files: [get(review, 'file', {})], + content: get(review, 'public', ''), }, ] - if (review.hasConfidential) { + if (get(review, 'confidential', '')) { comments.push({ public: false, content: review.confidential || undefined, @@ -129,39 +134,57 @@ export const parseReviewRequest = (review = {}) => { }) } 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 => { - dispatch(changeForm('reviewerReport', 'id', r.id)) - return dispatch(autosaveSuccess(get(r, 'updatedOn'))) - }) - .catch(e => dispatch(autosaveFailure(e))) + createRecommendation({ + fragmentId: version.id, + collectionId: project.id, + recommendation: omit(newValues, 'id'), + }).then( + r => { + dispatch(autosaveSuccess(Date.now())) + return r + }, + err => { + dispatch(autosaveFailure()) + throw err + }, + ) } - // } } export const onReviewChange = debounce(onChange, 1000, { maxWait: 5000 }) @@ -169,31 +192,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 9e0071c95..01aa93785 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 05f36f6d4..000000000 --- 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 9b530a316..154e043de 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 2be2e0d14..3d7f9913d 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/components-faraday/src/redux/files.js b/packages/components-faraday/src/redux/files.js index 623177c8a..02a36e851 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 49f8ecc81..fb3d21fef 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, -- GitLab