diff --git a/packages/component-faraday-ui/src/Textarea.js b/packages/component-faraday-ui/src/Textarea.js new file mode 100644 index 0000000000000000000000000000000000000000..3b53b3962f678050d46f65c6bb3e6fed1b5d63b8 --- /dev/null +++ b/packages/component-faraday-ui/src/Textarea.js @@ -0,0 +1,32 @@ +import { get } from 'lodash' +import { th } from '@pubsweet/ui-toolkit' +import styled, { css } from 'styled-components' + +import { marginHelper } from '.' + +const minHeight = props => css` + min-height: calc(${th('gridUnit')} * ${get(props, 'minHeight', 2)}); +` + +/** @component */ +export default styled.textarea` + border-radius: ${th('borderRadius')}; + border-color: ${({ hasError }) => + hasError ? th('colorError') : th('colorFurniture')}; + font-size: ${th('fontSizeBase')}; + font-family: ${th('fontWriting')}; + padding: ${th('gridUnit')}; + width: 100%; + + ${minHeight}; + ${marginHelper}; + + &:focus, + &:active { + outline: none; + } + + &:read-only { + background-color: ${th('colorBackgroundHue')}; + } +` diff --git a/packages/component-faraday-ui/src/Textarea.md b/packages/component-faraday-ui/src/Textarea.md new file mode 100644 index 0000000000000000000000000000000000000000..f8f79ac1fafb1f42a9df618431059f7ab738e9ca --- /dev/null +++ b/packages/component-faraday-ui/src/Textarea.md @@ -0,0 +1,5 @@ +A text area input. + +```js +<Textarea minHeight={10} /> +``` diff --git a/packages/component-faraday-ui/src/index.js b/packages/component-faraday-ui/src/index.js index 6368cb7fab1d4d7b3832480a52682dbb25f10bd9..d43b8dde054d6226113be72c44b24224834d5cbf 100644 --- a/packages/component-faraday-ui/src/index.js +++ b/packages/component-faraday-ui/src/index.js @@ -29,6 +29,7 @@ export { default as RemoteOpener } from './RemoteOpener' export { default as SortableList } from './SortableList' export { default as Tag } from './Tag' export { default as Text } from './Text' +export { default as Textarea } from './Textarea' export { default as WizardAuthors } from './WizardAuthors' export { default as WizardFiles } from './WizardFiles' diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.js b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.js new file mode 100644 index 0000000000000000000000000000000000000000..cee5fa1e74df06ae8f565ccda75718480f87b286 --- /dev/null +++ b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.js @@ -0,0 +1,103 @@ +import React from 'react' +import { has, get } from 'lodash' +import styled from 'styled-components' +import { reduxForm } from 'redux-form' +import { th } from '@pubsweet/ui-toolkit' +import { required } from 'xpub-validators' +import { compose, withProps } from 'recompose' +import { Button, Menu, ValidatedField } from '@pubsweet/ui' + +import { ContextualBox, OpenModal, Row, Item, Label, Textarea } from '../' + +const ManuscriptEicDecision = ({ + disabled, + isFetching, + formValues, + handleSubmit, + messagesLabel, + options = [], + ...rest +}) => ( + <ContextualBox label="Your Editorial Decision" startExpanded {...rest}> + <Root> + <Row justify="flex-start"> + <Item flex={0} vertical> + <Label required>Decision</Label> + <ValidatedField + component={input => <CutomMenu {...input} options={options} />} + name="decision" + validate={[required]} + /> + </Item> + </Row> + + <Row mt={6}> + <Item vertical> + <Label required> + { + messagesLabel[ + get(formValues, 'decision', 'return-to-handling-editor') + ] + } + </Label> + <ValidatedField + component={ValidatedTextArea} + name="message" + validate={[required]} + /> + </Item> + </Row> + + <Row justify="flex-end" mt={4}> + <OpenModal + isFetching={isFetching} + onConfirm={props => { + handleSubmit()(props) + }} + title="Are you sure you want to submit this decision?" + > + {showModal => ( + <Button + disabled={disabled} + onClick={showModal} + primary + size="medium" + > + SUBMIT DECISION + </Button> + )} + </OpenModal> + </Row> + </Root> + </ContextualBox> +) + +export default compose( + withProps(({ formValues }) => ({ + disabled: !has(formValues, 'decision') || !has(formValues, 'message'), + })), + reduxForm({ + form: 'eic-decision', + onSubmit: (values, disaptch, { submitDecision }) => modalProps => { + submitDecision(values, modalProps) + }, + }), +)(ManuscriptEicDecision) + +// #region styles +const Root = styled.div` + display: flex; + flex-direction: column; + padding: ${th('gridUnit')}; +` + +const CutomMenu = styled(Menu)` + min-width: calc(${th('gridUnit')} * 30); +` + +const ValidatedTextArea = styled(Textarea)` + & + div { + margin-top: 0; + } +` +// #endregion diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.md b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.md new file mode 100644 index 0000000000000000000000000000000000000000..2a71b354140859729174108668845b8a22a2fe50 --- /dev/null +++ b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptEicDecision.md @@ -0,0 +1,21 @@ +A contextual box for EiC decision. + +```js +const eicDecisions = [ + { value: 'return-to-handling-editor', label: 'Return to Handling Editor' }, + { value: 'publish', label: 'Publish' }, + { value: 'reject', label: 'Reject' }, +]; + +const messagesLabel = { + 'return-to-handling-editor': 'Comments for Handling Editor', + reject: 'Comments for Author', + publish: 'Comments for Author', +}; + +<ManuscriptEicDecision + messagesLabel={messagesLabel} + options={eicDecisions} + submitDecision={v => console.log('submit decision', v)} +/> +``` diff --git a/packages/component-faraday-ui/src/manuscriptDetails/index.js b/packages/component-faraday-ui/src/manuscriptDetails/index.js index ec99d00ceb081f970da76a66fc4c210aa693a3a6..42416b39d677f99e55a4c55fe0f644e1dfb06378 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/index.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/index.js @@ -4,3 +4,4 @@ export { default as ManuscriptHeader } from './ManuscriptHeader' export { default as ManuscriptMetadata } from './ManuscriptMetadata' export { default as ManuscriptFileList } from './ManuscriptFileList' export { default as ManuscriptAssignHE } from './ManuscriptAssignHE' +export { default as ManuscriptEicDecision } from './ManuscriptEicDecision' diff --git a/packages/component-faraday-ui/src/modals/OpenModal.js b/packages/component-faraday-ui/src/modals/OpenModal.js index 505a9fb073b3b7897af1b057196a5c392986a990..9c171e5fbf2fab2ba0577829c20c48cea49619ec 100644 --- a/packages/component-faraday-ui/src/modals/OpenModal.js +++ b/packages/component-faraday-ui/src/modals/OpenModal.js @@ -11,16 +11,8 @@ export default compose( modalComponent: single ? SingleActionModal : MultiAction, })), withHandlers({ - showModal: ({ - onConfirm = () => {}, - onCancel = () => {}, - ...rest - }) => () => { - rest.showModal({ - ...rest, - onConfirm: () => onConfirm(rest), - onCancel: () => onCancel(rest), - }) + showModal: props => () => { + props.showModal(props) }, }), )(OpenModal) diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index 23826bc0c3839ee0dd02c01ffbb86668f78b64b1..9e2d00191f821fcc72045888e0f782236475be54 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -1,5 +1,5 @@ import React, { Fragment } from 'react' -import { isEmpty } from 'lodash' +import { isEmpty, get, last } from 'lodash' import styled from 'styled-components' import { Text, @@ -8,8 +8,21 @@ import { ManuscriptAssignHE, ManuscriptMetadata, ManuscriptDetailsTop, + ManuscriptEicDecision, } from 'pubsweet-component-faraday-ui' +const eicDecisions = [ + { value: 'return-to-handling-editor', label: 'Return to Handling Editor' }, + { value: 'publish', label: 'Publish' }, + { value: 'reject', label: 'Reject' }, +] + +const messagesLabel = { + 'return-to-handling-editor': 'Comments for Handling Editor', + publish: 'Comments for Author', + reject: 'Comments for Author', +} + const ManuscriptLayout = ({ history, assignHE, @@ -18,6 +31,7 @@ const ManuscriptLayout = ({ getSignedUrl, editorInChief, handlingEditors, + createRecommendation, hasResponseToReviewers, editorialRecommendations, journal = {}, @@ -25,6 +39,7 @@ const ManuscriptLayout = ({ fragment = {}, permissions, isFetching, + formValues, }) => ( <Root> {!isEmpty(collection) && !isEmpty(fragment) ? ( @@ -66,6 +81,21 @@ const ManuscriptLayout = ({ isFetching={isFetching.editorsFetching} toggle={toggle} /> + + {permissions.canMakeDecision && ( + <ManuscriptEicDecision + formValues={get(formValues, 'eicDecision')} + isFetching={isFetching.recommendationsFetching} + messagesLabel={messagesLabel} + mt={2} + options={ + get(collection, 'status', 'submitted') === 'submitted' + ? [last(eicDecisions)] + : eicDecisions + } + submitDecision={createRecommendation} + /> + )} </Fragment> )} </RemoteOpener> diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 8e355cbf231d85eed5aa5ec5f85b57e6f5c80547..c1128792892f84c3c6600e0528624dbc3a42c0b2 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -5,6 +5,7 @@ import { withJournal } from 'xpub-journal' import { head, get, isEmpty } from 'lodash' import { replace } from 'react-router-redux' import { withRouter } from 'react-router-dom' +import { getFormValues } from 'redux-form' import { selectFragment, selectCollection, @@ -38,7 +39,7 @@ import { } from 'pubsweet-component-faraday-selectors' import ManuscriptLayout from './ManuscriptLayout' -import { parseSearchParams, redirectToError } from './utils' +import { parseEicDecision, parseSearchParams, redirectToError } from './utils' import { canAssignHE, selectFetching, @@ -47,6 +48,10 @@ import { revokeHandlingEditor, selectHandlingEditors, } from '../redux/editors' +import { + createRecommendation, + recommendationsFetching, +} from '../redux/recommendations' export default compose( setDisplayName('ManuscriptPage'), @@ -75,6 +80,7 @@ export default compose( clearCustomError, reviewerDecision, assignHandlingEditor, + createRecommendation, revokeHandlingEditor, getFragment: actions.getFragment, getCollection: actions.getCollection, @@ -91,6 +97,7 @@ export default compose( }, isFetching: { editorsFetching: selectFetching(state), + recommendationsFetching: recommendationsFetching(state), }, permissions: { canMakeRevision: canMakeRevision(state, collection, fragment), @@ -99,6 +106,9 @@ export default compose( canOverrideTechChecks: canOverrideTechnicalChecks(state, collection), canMakeRecommendation: canMakeRecommendation(state, collection, fragment), }, + formValues: { + eicDecision: getFormValues('eic-decision')(state), + }, })), ConnectPage(({ currentUser, handlingEditors, collection }) => { if (currentUser.isEIC) { @@ -150,6 +160,28 @@ export default compose( modalProps.hideModal() }) .catch(() => modalProps.setModalError('Oops! Something went wrong.')), + createRecommendation: ({ + fragment, + collection, + getFragment, + getCollection, + createRecommendation, + }) => (values, modalProps) => { + const recommendation = parseEicDecision(values) + createRecommendation({ + recommendation, + fragmentId: fragment.id, + collectionId: collection.id, + }) + .then(() => { + getCollection({ id: collection.id }) + getFragment(collection, fragment) + modalProps.hideModal() + }) + .catch(() => { + modalProps.setModalError('Oops! Something went wrong.') + }) + }, }), lifecycle({ componentDidMount() { diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js index bbe6eb5ec3fefc5575447214e3279fbea151b2cf..290712b167b1f6f1ca6dcc110227a39c41df5d21 100644 --- a/packages/component-manuscript/src/components/utils.js +++ b/packages/component-manuscript/src/components/utils.js @@ -266,3 +266,15 @@ export const requiredFiles = (values, formValues) => { } return undefined } + +// new stuff +export const parseEicDecision = ({ decision, message }) => ({ + recommendation: decision, + recommendationType: 'editorRecommendation', + comments: [ + { + public: decision !== 'return-to-handling-editor', + content: message, + }, + ], +}) diff --git a/packages/component-manuscript/src/index.js b/packages/component-manuscript/src/index.js index 01aa93785c2d01ec0fb22a177c2c6a123b2e71b0..9e0071c956db532671fb8e5e7b01b5a936ab193d 100644 --- a/packages/component-manuscript/src/index.js +++ b/packages/component-manuscript/src/index.js @@ -3,6 +3,7 @@ module.exports = { components: [() => require('./components')], reducers: { editors: () => require('./redux/editors').default, + recommendations: () => require('./redux/recommendations').default, }, }, } diff --git a/packages/component-manuscript/src/redux/recommendations.js b/packages/component-manuscript/src/redux/recommendations.js new file mode 100644 index 0000000000000000000000000000000000000000..9b530a3169845e974339dbf861236b00db09d9a3 --- /dev/null +++ b/packages/component-manuscript/src/redux/recommendations.js @@ -0,0 +1,78 @@ +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`, []) +export const selectEditorialRecommendations = (state, fragmentId) => + selectRecommendations(state, fragmentId).filter( + r => r.recommendationType === 'editorRecommendation' && r.comments, + ) +export const selectReviewRecommendations = (state, fragmentId) => + selectRecommendations(state, fragmentId).filter( + r => r.recommendationType === 'review', + ) + +export const recommendationsFetching = state => + get(state, 'recommendations', false) +// #endregion + +// #region Actions +// error handling and fetching is handled by the autosave reducer +export const createRecommendation = ({ + fragmentId, + collectionId, + recommendation, +}) => dispatch => { + dispatch(recommendationRequest()) + return create( + `/collections/${collectionId}/fragments/${fragmentId}/recommendations`, + recommendation, + ).then( + res => { + dispatch(recommendationDone()) + return res + }, + err => { + dispatch(recommendationDone()) + throw err + }, + ) +} + +export const updateRecommendation = ( + collId, + fragId, + recommendation, +) => dispatch => { + dispatch(recommendationRequest()) + return update( + `/collections/${collId}/fragments/${fragId}/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/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js index daf2f46d9f92f960ea7d08dbb00a95331ab3caad..aebc901864fd5ce67fd587a4b69e4ee908129b3a 100644 --- a/packages/xpub-faraday/config/default.js +++ b/packages/xpub-faraday/config/default.js @@ -46,7 +46,7 @@ module.exports = { API_ENDPOINT: '/api', baseUrl: process.env.CLIENT_BASE_URL || 'http://localhost:3000', 'login-redirect': '/', - 'redux-log': false, // process.env.NODE_ENV !== 'production', + 'redux-log': true, // process.env.NODE_ENV !== 'production', theme: process.env.PUBSWEET_THEME, }, orcid: {