diff --git a/packages/component-manuscript-manager/src/TechnicalChecks.js b/packages/component-manuscript-manager/src/TechnicalChecks.js new file mode 100644 index 0000000000000000000000000000000000000000..67e50c27bb0fdc03bc85a42670a1495d523c1994 --- /dev/null +++ b/packages/component-manuscript-manager/src/TechnicalChecks.js @@ -0,0 +1,30 @@ +const bodyParser = require('body-parser') + +const TechnicalChecks = app => { + app.use(bodyParser.json()) + const basePath = '/api/collections/:collectionId/status' + const routePath = './routes/technicalChecks' + + /** + * @api {patch} /api/collections/:collectionId/status Take a EQS or EQA decision. + * @apiGroup TechnicalChecks + * @apiParam {collectionId} collectionId Collection id + * * @apiParamExample {json} Body + * { + * "token": "12345", + * "agree": true, + * "step": "EQS" + * } + * @apiSuccessExample {json} Success + * HTTP/1.1 200 OK + * { + * } + * @apiErrorExample {json} Invite user errors + * HTTP/1.1 400 Bad Request + * HTTP/1.1 404 Not Found + * HTTP/1.1 500 Internal Server Error + */ + app.patch(basePath, require(`${routePath}/patch`)(app.locals.models)) +} + +module.exports = TechnicalChecks diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js new file mode 100644 index 0000000000000000000000000000000000000000..e569f8a7a07c7066eb32866a054280ed70a6df3e --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js @@ -0,0 +1,48 @@ +const { get, isEmpty } = require('lodash') +const { services } = require('pubsweet-component-helper-service') + +const TECHNICAL_STEPS = { + EQS: 'eqs', + EQA: 'eqa', +} + +const setNewStatus = (step, agree) => { + if (step === TECHNICAL_STEPS.EQS) { + return agree ? 'submitted' : 'rejected' + } else if (step === TECHNICAL_STEPS.EQA) { + return agree ? 'accepted' : 'rejected' + } +} + +module.exports = ({ Collection }) => async (req, res) => { + const { collectionId } = req.params + const { token, agree, step } = req.body + + try { + const collection = await Collection.find(collectionId) + const technicalCheckToken = get(collection, `technicalChecks.token`) + + if (isEmpty(technicalCheckToken)) { + return res.status(400).json({ + error: `Manuscript already handled.`, + }) + } + + if (technicalCheckToken !== token) { + return res.status(400).json({ + error: `Invalid token.`, + }) + } + + delete collection.technicalChecks.token + collection.status = setNewStatus(step, agree) + await collection.save() + + return res.status(200).json(collection) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'Item') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } +} diff --git a/packages/component-modal/src/components/withModal.js b/packages/component-modal/src/components/withModal.js index 35bb923623393be380a2af970af01ada1733a88e..685cc748a2b10cbb55dd7ebdfaa4496ec8f3442b 100644 --- a/packages/component-modal/src/components/withModal.js +++ b/packages/component-modal/src/components/withModal.js @@ -28,6 +28,7 @@ const withModal = mapperFn => BaseComponent => hideModal, modalProps, modalError, + isFetching, setModalError, modalsVisibility, ...rest @@ -39,6 +40,7 @@ const withModal = mapperFn => BaseComponent => {...modalProps} component={Component} hideModal={hideModal} + isFetching={isFetching} modalError={modalError} overlayColor={overlayColor} setModalError={setModalError} diff --git a/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js b/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js index 79651f8428c8ddbf7197b4826e7402abd4c4bbc5..4e3f753ed0e2a52df80411ea6cee56c71a5f5352 100644 --- a/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js +++ b/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js @@ -1,5 +1,6 @@ import React from 'react' import { isEmpty } from 'lodash' +import { connect } from 'react-redux' import { Button } from '@pubsweet/ui' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' @@ -18,11 +19,21 @@ import { import { Err, Subtitle } from './FormItems' import { parseSearchParams } from '../utils' +import { + technicalDecision, + technicalCheckFetcing, +} from '../../redux/technicalCheck' -const EQSDecisionPage = ({ id, eqsDecision, errorMessage, successMessage }) => ( +const EQSDecisionPage = ({ + params, + eqsDecision, + errorMessage, + successMessage, +}) => ( <Root> <Title> - Take a decision for manuscript <b>{id}</b>. + Take a decision for manuscript <b>{params.customId}</b>. + {params.collectionId} </Title> {errorMessage && <Err>{errorMessage}</Err>} {successMessage && <Subtitle>{successMessage}</Subtitle>} @@ -40,17 +51,30 @@ const EQSDecisionPage = ({ id, eqsDecision, errorMessage, successMessage }) => ( export default compose( setDisplayName('EQS Decision page'), - withModal(() => ({ + connect( + state => ({ + isFetching: technicalCheckFetcing(state), + }), + { technicalDecision }, + ), + withModal(({ isFetching }) => ({ + isFetching, modalComponent: ConfirmationModal, })), - withState('id', 'setId', null), + withState('params', 'setParams', { + token: null, + customId: null, + collectionId: null, + }), withState('successMessage', 'setSuccess', ''), withState('errorMessage', 'setError', ''), lifecycle({ componentDidMount() { - const { location, setId } = this.props - const { customId } = parseSearchParams(location.search) - setId(customId) + const { location, setParams } = this.props + const { customId, collectionId, token } = parseSearchParams( + location.search, + ) + setParams({ customId, collectionId, token }) }, }), withHandlers({ @@ -59,19 +83,32 @@ export default compose( showModal, hideModal, setSuccess, + technicalDecision, + params: { collectionId, token }, }) => decision => () => { showModal({ title: `Are you sure you want to ${ decision ? 'accept' : 'reject' } this EQS package?`, onConfirm: () => { - setSuccess('Manuscript accepted.') - hideModal() - }, - onCancel: () => { - setError('There was an error. Please try again.') - hideModal() + technicalDecision({ + step: 'eqs', + agree: decision, + collectionId, + token, + }) + .then(() => { + setSuccess( + 'Manuscript accepted. Thank you for your technical check!', + ) + hideModal() + }) + .catch(() => { + setError('There was an error. Please try again.') + hideModal() + }) }, + onCancel: hideModal, }) }, }), diff --git a/packages/components-faraday/src/index.js b/packages/components-faraday/src/index.js index b40accf6eaee709b192b849b8ab1290c47363428..be69a5315674890955a7de5fa07fcbfdd22bf9a5 100644 --- a/packages/components-faraday/src/index.js +++ b/packages/components-faraday/src/index.js @@ -7,6 +7,7 @@ module.exports = { editors: () => require('./redux/editors').default, files: () => require('./redux/files').default, reviewers: () => require('./redux/reviewers').default, + technicalCheck: () => require('./redux/technicalCheck').default, }, }, } diff --git a/packages/components-faraday/src/redux/index.js b/packages/components-faraday/src/redux/index.js index 9e0c549f2498178e77b7c129e3ce4f0ee5c53bae..c7fd43a9557173a35047de82ce6f5e64f18a8e03 100644 --- a/packages/components-faraday/src/redux/index.js +++ b/packages/components-faraday/src/redux/index.js @@ -3,4 +3,5 @@ export { default as errors } from './errors' export { default as authors } from './authors' export { default as editors } from './editors' export { default as reviewers } from './reviewers' +export { default as technicalCheck } from './technicalCheck' export { default as recommendations } from './recommendations' diff --git a/packages/components-faraday/src/redux/technicalCheck.js b/packages/components-faraday/src/redux/technicalCheck.js new file mode 100644 index 0000000000000000000000000000000000000000..8cb3a074bf9eed5220f56f7f476a7510cda8dc57 --- /dev/null +++ b/packages/components-faraday/src/redux/technicalCheck.js @@ -0,0 +1,67 @@ +import { get } from 'lodash' +import { update } from 'pubsweet-client/src/helpers/api' + +const DECISION_REQUEST = 'tc/DECISION_REQUEST' +const DECISION_SUCCESS = 'tc/DECISION_SUCCESS' +const DECISION_ERROR = 'tc/DECISION_ERROR' + +const decisionRequest = () => ({ + type: DECISION_REQUEST, +}) + +const decisionSuccess = () => ({ + type: DECISION_SUCCESS, +}) + +const decisionError = error => ({ + type: DECISION_ERROR, + error, +}) + +export const technicalDecision = ({ + step, + agree, + token, + collectionId, +}) => dispatch => { + dispatch(decisionRequest()) + return update(`/collections/${collectionId}/status`, { + step, + token, + agree, + }).then( + r => { + dispatch(decisionSuccess()) + return r + }, + err => { + dispatch(decisionError(err)) + throw err + }, + ) +} + +export const technicalCheckFetcing = state => + get(state, 'technicalCheck.fetching', false) + +export default (state = {}, action = {}) => { + switch (action.type) { + case DECISION_REQUEST: + return { + ...state, + fetching: true, + } + case DECISION_SUCCESS: + return { + error: null, + fetching: false, + } + case DECISION_ERROR: + return { + error: action.error, + fetching: false, + } + default: + return state + } +}