import React, { Fragment } from 'react' import { connect } from 'react-redux' import { isEmpty, merge } from 'lodash' import { withJournal } from 'xpub-journal' import { required } from 'xpub-validators' import styled, { css } from 'styled-components' import { Menu, Icon, Button, Spinner, ErrorText, ValidatedField, } from '@pubsweet/ui' import { th } from '@pubsweet/ui-toolkit' import { compose, withHandlers, withProps } from 'recompose' import { reduxForm, isSubmitting, getFormValues, change as changeForm, } from 'redux-form' import AutosaveIndicator from 'pubsweet-component-wizard/src/components/AutosaveIndicator' import { uploadFile, deleteFile, getSignedUrl, getRequestStatus, } from 'pubsweet-components-faraday/src/redux/files' import { FileItem, FilePicker, } from 'pubsweet-components-faraday/src/components/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="Select" /> )} name="recommendation" validate={[required]} /> </Row> <Spacing /> <Row left> <Label>Report</Label> {!fileFetching.review ? ( <Fragment> {isEmpty(formValues.files) && ( <FilePicker allowedFileExtensions={['pdf', 'doc', 'docx']} onUpload={addFile} > <ActionText left={12}>Upload file</ActionText> </FilePicker> )} </Fragment> ) : ( <Spinner size={2} /> )} </Row> <Row> <FullWidth className="full-width"> <ValidatedField component={TextAreaField} name="public" validate={isEmpty(formValues.files) ? [required] : []} /> </FullWidth> </Row> {formValues.files && ( <Fragment> <Row left> {formValues.files.map(file => ( <FileItem compact id={file.id} key={file.id} {...file} removeFile={removeFile} /> ))} </Row> </Fragment> )} {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> <AutosaveIndicator formName="reviewerReport" lastUpdated={review.updatedOn} /> </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