diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index 6508a91a4921237406bd5b9e386e167f1e608977..f1d9bcb1960a84d6656d68c25bf8616b2b4c02a8 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -7,7 +7,11 @@ export const isHEToManuscript = (state, collectionId) => { return get(collection, 'handlingEditor.id') === currentUserId } -const canMakeRecommendationStatuses = ['reviewCompleted', 'heAssigned'] +const canMakeRecommendationStatuses = [ + 'reviewCompleted', + 'heAssigned', + 'underReview', +] export const canMakeRecommendation = (state, collection, fragment = {}) => { if (fragment.id !== last(collection.fragments)) return false const isHE = isHEToManuscript(state, collection.id) @@ -75,13 +79,22 @@ export const canMakeDecision = (state, collection, fragment = {}) => { return isEIC && canMakeDecisionStatuses.includes(status) } -const canEditManuscriptStatuses = ['draft', 'technicalChecks'] +const canEditManuscriptStatuses = ['draft', 'technicalChecks', 'inQA'] export const canEditManuscript = (state, collection, fragment = {}) => { - if (fragment.id !== last(collection.fragments)) return false + const isAdmin = currentUserIs(state, 'isAdmin') + if (!isAdmin || fragment.id !== last(collection.fragments)) return false const status = get(collection, 'status') + return canEditManuscriptStatuses.includes(status) +} + +const canOverrideTechnicalChecksStatuses = ['technicalChecks', 'inQA'] +export const canOverrideTechnicalChecks = (state, collection) => { const isAdmin = currentUserIs(state, 'isAdmin') - return isAdmin && canEditManuscriptStatuses.includes(status) + if (!isAdmin) return false + const status = get(collection, 'status') + + return canOverrideTechnicalChecksStatuses.includes(status) } export const canSeeReviewersReports = (state, collectionId) => { diff --git a/packages/component-fixture-manager/src/fixtures/collections.js b/packages/component-fixture-manager/src/fixtures/collections.js index c6d401815491578a0262f7cb8cb06b2e3ed66c56..84583485feff68c50c532e4a5f4a396147177df2 100644 --- a/packages/component-fixture-manager/src/fixtures/collections.js +++ b/packages/component-fixture-manager/src/fixtures/collections.js @@ -11,7 +11,7 @@ const collections = { type: 'collection', fragments: [fragment.id], owners: [user.id], - save: jest.fn(), + save: jest.fn(() => collections.collection), invitations: [ { id: chance.guid(), @@ -44,6 +44,7 @@ const collections = { technicalChecks: { token: chance.guid(), }, + status: 'pendingApproval', }, } diff --git a/packages/component-helper-service/src/services/User.js b/packages/component-helper-service/src/services/User.js index 39d03896853ea00f5bb81425bdda1bb551f7af7f..09c722d31cabe70492fb2c0fcdae381ee4c0c1ef 100644 --- a/packages/component-helper-service/src/services/User.js +++ b/packages/component-helper-service/src/services/User.js @@ -72,7 +72,8 @@ class User { async getEiCName() { const editorsInChief = await this.getEditorsInChief() - const { firstName, lastName } = editorsInChief[0] + const firstName = get(editorsInChief, '0.firstName', 'Editor') + const lastName = get(editorsInChief, '0.lastName', 'in Chief') return `${firstName} ${lastName}` } } diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js index d4046f5a95f38e0c56bca849133118d98ccbd421..5b3840f79d5c78f1d6abedfb513c89b086f06737 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js @@ -1,4 +1,5 @@ const getEmailCopy = ({ + customId, emailType, titleText, comments = '', @@ -19,7 +20,6 @@ const getEmailCopy = ({ break case 'author-manuscript-published': paragraph = `I am delighted to inform you that ${titleText} has passed through the review process and will be published in Hindawi.<br/><br/> - ${comments}<br/><br/> Thanks again for choosing to publish with us.` hasLink = false break @@ -77,6 +77,9 @@ const getEmailCopy = ({ paragraph = `In order for ${titleText} to proceed to publication, there needs to be a revision. <br/><br/> For more information about what is required, please visit the manuscript details page.` break + case 'eqa-manuscript-request-for-approval': + paragraph = `Manuscript ID ${customId} has passed peer-review and is now ready for EQA. Please click on the link below to either approve or return the manuscript to the Editor in Chief:` + break default: throw new Error(`The ${emailType} email type is not defined.`) } 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 b3f2e877a4aad364f605e362495d370193328932..c86bbd35a818229fbc82d1992ee27cbe9e91b00e 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js @@ -10,10 +10,12 @@ const { const { getEmailCopy } = require('./emailCopy') +const editorialAssistantEmail = config.get('mailer.editorialAssistant') const unsubscribeSlug = config.get('unsubscribe.url') module.exports = { async sendNotifications({ + hasEQA, baseUrl, fragment, UserModel, @@ -33,12 +35,15 @@ module.exports = { fragmentAuthors.submittingAuthor.firstName } ${fragmentAuthors.submittingAuthor.lastName}` + const userHelper = new User({ UserModel }) + const eicName = await userHelper.getEiCName() + const email = new Email({ type: 'user', content: { unsubscribeLink: baseUrl, ctaText: 'MANUSCRIPT DETAILS', - signatureName: get(collection, 'handlingEditor.name', 'N/A'), + signatureName: eicName, ctaLink: services.createUrl( baseUrl, `/projects/${collection.id}/versions/${fragment.id}/details`, @@ -46,78 +51,93 @@ module.exports = { }, }) - const userHelper = new User({ UserModel }) - - let comments - if (isEditorInChief) { - const eicComments = chain(newRecommendation) - .get('comments') - .find(comm => !comm.public) - .get('content') - .value() - - comments = eicComments - } - - const hasPeerReview = (collection = {}) => - !isEmpty(collection.handlingEditor) - if ( - (isEditorInChief || newRecommendation.recommendationType === 'review') && - hasPeerReview(collection) + !hasEQA && + isEditorInChief && + newRecommendation.recommendation === 'publish' ) { - // the request came from either the Editor in Chief or a reviewer, so the HE needs to be notified - sendHandlingEditorEmail({ + sendEQAEmail({ email, - eicName: await userHelper.getEiCName(), + eicName, baseUrl, - comments, - titleText, - targetUserName, + collection, subjectBaseText, - handlingEditor: get(collection, 'handlingEditor', {}), - recommendation: newRecommendation.recommendation, - recommendationType: newRecommendation.recommendationType, }) - } - - if ( - newRecommendation.recommendationType !== 'review' && - newRecommendation.recommendation !== 'return-to-handling-editor' - ) { - if (isEditorInChief || collection.status === 'revisionRequested') { - sendAuthorsEmail({ - email, - baseUrl, - titleText, - parsedFragment, - fragmentAuthors, - isEditorInChief, - subjectBaseText, - newRecommendation, - }) + } else { + let comments + if (isEditorInChief) { + const eicComments = chain(newRecommendation) + .get('comments') + .find(comm => !comm.public) + .get('content') + .value() + + comments = eicComments } - if (hasPeerReview(collection)) { - sendReviewersEmail({ + + const hasPeerReview = (collection = {}) => + !isEmpty(collection.handlingEditor) + + if ( + (isEditorInChief || + newRecommendation.recommendationType === 'review') && + hasPeerReview(collection) + ) { + // the request came from either the Editor in Chief or a reviewer, so the HE needs to be notified + sendHandlingEditorEmail({ email, + eicName: await userHelper.getEiCName(), baseUrl, - UserModel, + comments, titleText, - fragmentHelper, - isEditorInChief, + targetUserName, subjectBaseText, + handlingEditor: get(collection, 'handlingEditor', {}), recommendation: newRecommendation.recommendation, - handlingEditorName: get(collection, 'handlingEditor.name', 'N/A'), + recommendationType: newRecommendation.recommendationType, }) + } - sendEiCsEmail({ - email, - baseUrl, - userHelper, - titleText, - subjectBaseText, - recommendation: newRecommendation, - }) + if ( + newRecommendation.recommendationType !== 'review' && + newRecommendation.recommendation !== 'return-to-handling-editor' + ) { + if (isEditorInChief || collection.status === 'revisionRequested') { + sendAuthorsEmail({ + email, + baseUrl, + titleText, + parsedFragment, + fragmentAuthors, + isEditorInChief, + subjectBaseText, + newRecommendation, + handlingEditorName: get(collection, 'handlingEditor.name', eicName), + }) + } + + if (hasPeerReview(collection)) { + sendReviewersEmail({ + email, + baseUrl, + UserModel, + titleText, + fragmentHelper, + isEditorInChief, + subjectBaseText, + recommendation: newRecommendation.recommendation, + handlingEditorName: get(collection, 'handlingEditor.name', eicName), + }) + + sendEiCsEmail({ + email, + baseUrl, + userHelper, + titleText, + subjectBaseText, + recommendation: newRecommendation, + }) + } } } }, @@ -163,7 +183,9 @@ const sendHandlingEditorEmail = ({ email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { id: handlingEditor.id, }) + email.content.signatureName = eicName + const { html, text } = email.getBody({ body: getEmailCopy({ emailType, @@ -183,6 +205,7 @@ const sendAuthorsEmail = async ({ subjectBaseText, fragmentAuthors, newRecommendation, + handlingEditorName, parsedFragment: { heRecommendation }, }) => { let emailType, authors, comments @@ -216,7 +239,10 @@ const sendAuthorsEmail = async ({ })) } else { emailType = 'author-request-to-revision' + email.content.subject = `${subjectBaseText} Recommendation` + email.content.signatureName = handlingEditorName + const authorNote = newRecommendation.comments.find(comm => comm.public) const content = get(authorNote, 'content') const authorNoteText = content ? `Reason & Details: "${content}"` : '' @@ -386,6 +412,43 @@ const sendEiCsEmail = async ({ }) } +const sendEQAEmail = ({ + email, + eicName, + baseUrl, + collection, + subjectBaseText, +}) => { + const emailType = 'eqa-manuscript-request-for-approval' + + email.toUser = { + email: editorialAssistantEmail, + name: 'Editorial Assistant', + } + + email.content.unsubscribeLink = baseUrl + email.content.signatureName = eicName + email.content.subject = `${subjectBaseText} Request for EQA Approval` + email.content.ctaLink = services.createUrl( + baseUrl, + config.get('eqa-decision.url'), + { + collectionId: collection.id, + customId: collection.customId, + token: collection.technicalChecks.token, + }, + ) + email.content.ctaText = 'MAKE DECISION' + + const { html, text } = email.getBody({ + body: getEmailCopy({ + emailType, + customId: collection.customId, + }), + }) + email.sendEmail({ html, text }) +} + const getSubjectByRecommendation = recommendation => ['minor', 'major'].includes(recommendation) ? 'Revision Requested' diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index 69135f088fdff18442320398441a8db3f40562d5..f67754387a6deef1b7dd8f2d93f31221c7bd52d5 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -1,6 +1,7 @@ const uuid = require('uuid') -const { pick, get } = require('lodash') +const { pick, get, set, has } = require('lodash') const config = require('config') +const { v4 } = require('uuid') const { services, @@ -75,7 +76,10 @@ module.exports = models => async (req, res) => { fragment.revision = pick(fragment, ['authors', 'files', 'metadata']) } - if (isEditorInChief && recommendation === 'publish') { + const technicalChecks = get(collection, 'technicalChecks', {}) + const { hasEQA } = technicalChecks + + if (isEditorInChief && recommendation === 'publish' && !hasEQA) { const { journal, xmlParser, ftp } = mtsConfig const MTS = new MTSService(journal, xmlParser, s3Config, ftp) const packageFragment = { @@ -87,9 +91,28 @@ module.exports = models => async (req, res) => { } await MTS.sendPackage({ fragment: packageFragment, isEQA: true }) + + collection.status = 'inQA' + set(collection, 'technicalChecks.token', v4()) + set(collection, 'technicalChecks.hasEQA', false) + await collection.save() + } + + /* if the EiC returns the manuscript to the HE after the EQA has been performed + then remove all properties from the technicalChecks property so that the manuscript + can go through the EQA process again + */ + if ( + isEditorInChief && + recommendation === 'return-to-handling-editor' && + has(collection.technicalChecks, 'hasEQA') + ) { + collection.technicalChecks = {} + await collection.save() } notifications.sendNotifications({ + hasEQA, fragment, collection, isEditorInChief, diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js index c562e8be47b57e98d965e88681150adead6057b0..0c36acca52a116cc5f9584d0679a52944e635b02 100644 --- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js +++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js @@ -1,14 +1,34 @@ -const getEmailCopy = ({ emailType, titleText }) => { +const getEmailCopy = ({ emailType, titleText, comments }) => { let paragraph + let hasLink = true switch (emailType) { case 'eqs-manuscript-accepted': paragraph = `We are please to inform you that ${titleText} has passed the Hindawi technical check process and is now submitted. Please click the link below to access the manuscript.` break + case 'he-manuscript-published': + hasLink = false + paragraph = `Thank you for your recommendation to publish ${titleText} based on the reviews you received.<br/><br/> + I can confirm this article will now go through to publication.` + break + case 'author-manuscript-published': + paragraph = `I am delighted to inform you that ${titleText} has passed through the review process and will be published in Hindawi.<br/><br/> + Thanks again for choosing to publish with us.` + hasLink = false + break + case 'submitted-reviewers-after-publish': + hasLink = false + paragraph = `Thank you for your review on ${titleText}. After taking into account the reviews and the recommendation of the Handling Editor, I can confirm this article will now be published.<br/><br/> + If you have any queries about this decision, then please email them to Hindawi as soon as possible.` + break + case 'eqa-manuscript-returned-to-eic': + paragraph = `We regret to inform you that ${titleText} has been returned with comments. Please click the link below to access the manuscript.<br/><br/> + Comments: ${comments}<br/><br/>` + break default: throw new Error(`The ${emailType} email type is not defined.`) } - return { paragraph, hasLink: true } + return { paragraph, hasLink } } module.exports = { diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js index 782cdbb0c0325048800a2f12096f013319fd8b8f..89cc1d71180c06f2fed6c78f2650a974ec8501ea 100644 --- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js +++ b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js @@ -1,3 +1,6 @@ +const config = require('config') +const { get } = require('lodash') + const { User, Email, @@ -6,10 +9,15 @@ const { } = require('pubsweet-component-helper-service') const { getEmailCopy } = require('./emailCopy') +const unsubscribeSlug = config.get('unsubscribe.url') + module.exports = { async sendNotifications({ + isEQA, + agree, baseUrl, collection, + comments = '', User: UserModel, Fragment: FragmentModel, }) { @@ -18,21 +26,29 @@ module.exports = { const parsedFragment = await fragmentHelper.getFragmentData({ handlingEditor: collection.handlingEditor, }) - const { submittingAuthor } = await fragmentHelper.getAuthorData({ + + const { + activeAuthors: authors, + submittingAuthor, + } = await fragmentHelper.getAuthorData({ UserModel, }) const titleText = `the manuscript titled "${parsedFragment.title}" by ${ submittingAuthor.firstName } ${submittingAuthor.lastName}` + const subjectBaseText = `${collection.customId}: Manuscript` const userHelper = new User({ UserModel }) + const subject = `${subjectBaseText} ${ + agree ? '' : 'Not ' + }Passed Technical Checks` const email = new Email({ type: 'user', content: { - subject: `${collection.customId}: Manuscript Passed Technical Checks`, - signatureName: 'EQS Team', + subject, + signatureName: 'EQA Team', ctaLink: services.createUrl( baseUrl, `/projects/${collection.id}/versions/${fragment.id}/details`, @@ -42,23 +58,173 @@ module.exports = { }, }) - const editors = (await userHelper.getEditorsInChief()).map(eic => ({ - ...eic, - ...getEmailCopy({ - emailType: 'eqs-manuscript-accepted', + if (isEQA && agree) { + const eicName = await userHelper.getEiCName() + email.content.signatureName = eicName + + sendAuthorsEmail({ + email, + baseUrl, titleText, - }), - })) - - editors.forEach(eic => { - email.toUser = { - email: eic.email, - name: `${eic.firstName} ${eic.lastName}`, - } - const { html, text } = email.getBody({ - body: { paragraph: eic.paragraph, hasLink: eic.hasLink }, + subjectBaseText, + fragmentAuthors: authors, }) - email.sendEmail({ html, text }) - }) + sendHandlingEditorEmail({ + email, + baseUrl, + titleText, + subjectBaseText, + handlingEditor: get(collection, 'handlingEditor', {}), + }) + sendSubmittedReviewersEmail({ + email, + baseUrl, + titleText, + UserModel, + fragmentHelper, + subjectBaseText, + }) + } else { + sendEditorsEmail({ email, agree, comments, userHelper, titleText }) + } }, } + +const sendEditorsEmail = async ({ + email, + agree, + comments = '', + userHelper, + titleText, +}) => { + const emailType = agree + ? 'eqs-manuscript-accepted' + : 'eqa-manuscript-returned-to-eic' + + const editors = (await userHelper.getEditorsInChief()).map(eic => ({ + ...eic, + ...getEmailCopy({ + emailType, + titleText, + comments, + }), + })) + + editors.forEach(eic => { + email.toUser = { + email: eic.email, + name: `${eic.firstName} ${eic.lastName}`, + } + const { html, text } = email.getBody({ + body: { paragraph: eic.paragraph, hasLink: eic.hasLink }, + }) + email.sendEmail({ html, text }) + }) +} + +const sendHandlingEditorEmail = ({ + email, + baseUrl, + titleText, + handlingEditor, + subjectBaseText, +}) => { + email.content.subject = `${subjectBaseText} Decision` + const emailType = 'he-manuscript-published' + + email.toUser = { + email: handlingEditor.email, + name: handlingEditor.name, + } + email.content.unsubscribeLink = services.createUrl(baseUrl, unsubscribeSlug, { + id: handlingEditor.id, + }) + + const { html, text } = email.getBody({ + body: getEmailCopy({ + emailType, + titleText, + }), + }) + email.sendEmail({ html, text }) +} + +const sendSubmittedReviewersEmail = async ({ + email, + baseUrl, + titleText, + UserModel, + fragmentHelper, + subjectBaseText, +}) => { + email.content.subject = `${subjectBaseText} Decision` + + const reviewers = (await fragmentHelper.getReviewers({ + UserModel, + type: 'submitted', + })).map(rev => ({ + ...rev, + ...getEmailCopy({ + emailType: 'submitted-reviewers-after-publish', + titleText, + }), + })) + + reviewers.forEach(reviewer => { + email.toUser = { + email: reviewer.email, + name: `${reviewer.firstName} ${reviewer.lastName}`, + } + email.content.unsubscribeLink = services.createUrl( + baseUrl, + unsubscribeSlug, + { + id: reviewer.id, + }, + ) + const { html, text } = email.getBody({ + body: { paragraph: reviewer.paragraph, hasLink: reviewer.hasLink }, + }) + email.sendEmail({ html, text }) + }) +} + +const sendAuthorsEmail = ({ + email, + baseUrl, + titleText, + subjectBaseText, + fragmentAuthors, +}) => { + const emailType = 'author-manuscript-published' + email.content.subject = `${subjectBaseText} Published` + + const authors = fragmentAuthors.map(author => ({ + ...author, + ...getEmailCopy({ + emailType, + titleText, + }), + })) + + authors.forEach(author => { + email.toUser = { + email: author.email, + name: `${author.firstName} ${author.lastName}`, + } + email.content.unsubscribeLink = services.createUrl( + baseUrl, + unsubscribeSlug, + { + id: author.id, + }, + ) + const { html, text } = email.getBody({ + body: { + paragraph: author.paragraph, + hasLink: author.hasLink, + }, + }) + email.sendEmail({ html, text }) + }) +} diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js index 934709a746946699d07062c71084b3013f4fff13..14e68a7cc4b745752a5ddd2d23a733876f0f9a85 100644 --- a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js +++ b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js @@ -12,13 +12,13 @@ const setNewStatus = (step, agree) => { if (step === TECHNICAL_STEPS.EQS) { return agree ? 'submitted' : 'rejected' } else if (step === TECHNICAL_STEPS.EQA) { - return agree ? 'accepted' : 'rejected' + return agree ? 'accepted' : 'pendingApproval' } } module.exports = ({ Collection, Fragment, User }) => async (req, res) => { const { collectionId } = req.params - const { token, agree, step } = req.body + const { token, agree, step, comments } = req.body try { const collection = await Collection.find(collectionId) @@ -37,17 +37,21 @@ module.exports = ({ Collection, Fragment, User }) => async (req, res) => { } delete collection.technicalChecks.token + if (step === TECHNICAL_STEPS.EQA) { + collection.technicalChecks.hasEQA = true + } collection.status = setNewStatus(step, agree) await collection.save() - if (agree) { - sendNotifications({ - User, - Fragment, - collection, - baseUrl: services.getBaseUrl(req), - }) - } + sendNotifications({ + User, + agree, + comments, + Fragment, + collection, + baseUrl: services.getBaseUrl(req), + isEQA: step === TECHNICAL_STEPS.EQA, + }) return res.status(200).json(collection) } catch (e) { diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js index 0f0f048e62835ecbd5d6c9df1f36ad3de740b183..686124cb9518df27b9c722fae0f645c764a4d415 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -11,6 +11,7 @@ const chance = new Chance() jest.mock('@pubsweet/component-send-email', () => ({ send: jest.fn(), })) +jest.mock('pubsweet-component-mts-package') const reqBody = { recommendation: 'accept', @@ -206,4 +207,129 @@ describe('Post fragments recommendations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Unauthorized.') }) + it('should return success when the EiC recommends to reject without peer review', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body.recommendation = 'reject' + body.recommendationType = 'editorRecommendation' + + delete fragment.recommendations + delete fragment.revision + delete fragment.invitations + delete collection.invitations + delete collection.handlingEditor + + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + + expect(data.userId).toEqual(editorInChief.id) + expect(data.recommendation).toBe('reject') + }) + it('should return success when the EiC recommends to publish without EQA', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body.recommendation = 'publish' + body.recommendationType = 'editorRecommendation' + delete collection.technicalChecks + + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + + expect(collection.status).toBe('inQA') + expect(collection.technicalChecks).toHaveProperty('hasEQA') + expect(collection.technicalChecks.hasEQA).toBeFalsy() + expect(data.userId).toEqual(editorInChief.id) + expect(data.recommendation).toBe('publish') + }) + it('should return success when the EiC recommends to publish with EQA accepted', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body.recommendation = 'publish' + body.recommendationType = 'editorRecommendation' + + collection.technicalChecks.hasEQA = true + + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + + expect(collection.status).toBe('accepted') + expect(data.userId).toEqual(editorInChief.id) + expect(data.recommendation).toBe('publish') + }) + it('should return success when the EiC returns the manuscript to HE with comments after EQA returned to EiC', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body.recommendation = 'return-to-handling-editor' + body.recommendationType = 'editorRecommendation' + body.comments = 'This needs more work' + + delete fragment.recommendations + delete fragment.revision + delete fragment.invitations + delete collection.invitations + delete collection.handlingEditor + collection.technicalChecks.hasEQA = false + + const res = await requests.sendRequest({ + body, + userId: editorInChief.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + + expect(collection.status).toBe('reviewCompleted') + expect(collection.technicalChecks).not.toHaveProperty('token') + expect(collection.technicalChecks).not.toHaveProperty('hasEQA') + + expect(data.userId).toEqual(editorInChief.id) + expect(data.recommendation).toBe('return-to-handling-editor') + }) }) diff --git a/packages/component-manuscript-manager/src/tests/technicalChecks/patch.test.js b/packages/component-manuscript-manager/src/tests/technicalChecks/patch.test.js index 37c2ca388c56320be9a1527db7aa22c6f310a002..f8b6ccd6bc92c2817aeb60b454c6347a7d6eb47b 100644 --- a/packages/component-manuscript-manager/src/tests/technicalChecks/patch.test.js +++ b/packages/component-manuscript-manager/src/tests/technicalChecks/patch.test.js @@ -29,7 +29,7 @@ describe('Patch technical checks route handler', () => { models = Model.build(testFixtures) }) - it('should return success when the parameters are correct', async () => { + it('should return success when the EQS is accepted', async () => { const { collection } = testFixtures.collections body.token = collection.technicalChecks.token @@ -46,6 +46,62 @@ describe('Patch technical checks route handler', () => { expect(res.statusCode).toBe(200) }) + it('should return success when the EQS is rejected', async () => { + const { collection } = testFixtures.collections + body.token = collection.technicalChecks.token + body.agree = false + + const res = await requests.sendRequest({ + body, + models, + route, + path, + params: { + collectionId: collection.id, + }, + }) + + expect(res.statusCode).toBe(200) + }) + + it('should return success when the EQA is accepted', async () => { + const { collection } = testFixtures.collections + body.token = collection.technicalChecks.token + body.step = 'eqa' + + const res = await requests.sendRequest({ + body, + models, + route, + path, + params: { + collectionId: collection.id, + }, + }) + + expect(res.statusCode).toBe(200) + }) + + it('should return success when the EQA is returned with comments', async () => { + const { collection } = testFixtures.collections + body.token = collection.technicalChecks.token + body.agree = false + body.step = 'eqa' + body.comments = 'suspicion of plagiarism' + + const res = await requests.sendRequest({ + body, + models, + route, + path, + params: { + collectionId: collection.id, + }, + }) + + expect(res.statusCode).toBe(200) + }) + it('should return an error when the collection does not exist', async () => { const res = await requests.sendRequest({ body, diff --git a/packages/component-manuscript/src/components/SideBarActions.js b/packages/component-manuscript/src/components/SideBarActions.js index dbfc3d7cd8c6bcbd6f7ce6a70176913185514c9a..ea233bbbc75e697bdfb16c76f7fbdb8c50ed3fcd 100644 --- a/packages/component-manuscript/src/components/SideBarActions.js +++ b/packages/component-manuscript/src/components/SideBarActions.js @@ -1,6 +1,6 @@ import React from 'react' import { compose, withHandlers } from 'recompose' -import { Icon, Button } from '@pubsweet/ui' +import { Icon } from '@pubsweet/ui' import { connect } from 'react-redux' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' @@ -15,6 +15,7 @@ import { canMakeDecision, canMakeRecommendation, canEditManuscript, + canOverrideTechnicalChecks, } from 'pubsweet-component-faraday-selectors/src' const SideBarActions = ({ @@ -23,26 +24,19 @@ const SideBarActions = ({ goToEdit, canMakeDecision, canEditManuscript, + goToTechnicalCheck, + canOverrideTechChecks, canMakeRecommendation, }) => ( <Root> - {canEditManuscript && ( - <Button - data-test="button-edit-manuscript" - onClick={goToEdit(project, version)} - primary - > - Edit - </Button> - )} {canMakeDecision && ( <Decision collectionId={project.id} fragmentId={version.id} modalKey={`decide-${version.id}`} + status={project.status} /> )} - {canMakeRecommendation && ( <Recommendation collectionId={project.id} @@ -50,6 +44,24 @@ const SideBarActions = ({ modalKey={`recommend-${version.id}`} /> )} + {canOverrideTechChecks && ( + <ClickableIcon + data-test="button-technical-checks" + onClick={goToTechnicalCheck(project)} + title="Technical Checks" + > + <Icon>check-square</Icon> + </ClickableIcon> + )} + {canEditManuscript && ( + <ClickableIcon + data-test="button-edit-manuscript" + onClick={goToEdit(project, version)} + title="Edit Manuscript" + > + <Icon>edit</Icon> + </ClickableIcon> + )} <ZipFiles archiveName={`ID-${project.customId}`} collectionId={project.id} @@ -68,6 +80,7 @@ export default compose( canMakeDecision: canMakeDecision(state, project, version), canMakeRecommendation: canMakeRecommendation(state, project, version), canEditManuscript: canEditManuscript(state, project, version), + canOverrideTechChecks: canOverrideTechnicalChecks(state, project), })), withHandlers({ goToEdit: ({ history }) => (project, version) => () => { @@ -75,6 +88,14 @@ export default compose( editMode: true, }) }, + goToTechnicalCheck: ({ history }) => project => () => { + const { status, id, customId, technicalChecks: { token = '' } } = project + const stage = status === 'technicalChecks' ? 'eqs' : 'eqa' + history.push({ + pathname: `/${stage}-decision`, + search: `?collectionId=${id}&customId=${customId}&token=${token}`, + }) + }, }), )(SideBarActions) diff --git a/packages/component-mts-package/src/PackageManager.js b/packages/component-mts-package/src/PackageManager.js index 967985f3a45c6170fbd6a4b5d2d627ebe8cac633..b98dbf10308c84b927b10d3f670b285a1b6ca8b7 100644 --- a/packages/component-mts-package/src/PackageManager.js +++ b/packages/component-mts-package/src/PackageManager.js @@ -17,30 +17,27 @@ const createFilesPackage = (s3Config, archiver = nodeArchiver) => { }) const s3 = new AWS.S3() const asyncGetObject = promisify(s3.getObject.bind(s3)) - const asyncListObjects = promisify(s3.listObjects.bind(s3)) return async ({ fragment, fileTypes, xmlFile, isEQA = false }) => { - const { id } = fragment + const { files = {} } = fragment let packageName = get(xmlFile, 'name', '').replace('.xml', '') if (isEQA) { packageName = `ACCEPTED_${packageName}` } try { - const params = { - Bucket: s3Config.bucket, - Prefix: `${id}`, - } - const s3Items = await asyncListObjects(params) - if (s3Items) { + const s3FileIDs = Object.values(files) + .reduce((acc, f) => [...acc, ...f], []) + .map(f => f.id) + + if (s3FileIDs) { const s3Files = await Promise.all( - s3Items.Contents.map(content => + s3FileIDs.map(fileID => asyncGetObject({ Bucket: s3Config.bucket, - Key: content.Key, + Key: fileID, }), ), ) - if (s3Files) { const packageOutput = fs.createWriteStream(`${packageName}.zip`) const archive = archiver('zip') diff --git a/packages/component-mts-package/tests/MTS.test.js b/packages/component-mts-package/tests/MTS.test.js index 6655fbe07f3602b353cb89be4371bde679d4ddff..42c67eafd272402699e9be402e3f9d3f1860e0a6 100644 --- a/packages/component-mts-package/tests/MTS.test.js +++ b/packages/component-mts-package/tests/MTS.test.js @@ -27,7 +27,7 @@ describe('MTS integration', () => { }) it('should contain configured journal name ', () => { - const result = MTS.composeJson(mocks.fragment) + const result = MTS.composeJson({ fragment: mocks.fragment }) expect(result).toHaveProperty( 'article.front.journal-meta.journal-title-group.journal-title._text', 'Bioinorganic Chemistry and Applications', diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/emails/notifications.js b/packages/component-user-manager/src/routes/fragmentsUsers/emails/notifications.js index 9a7af8dbcd3819870c4d749ba3c85cd910db219b..9a9281fa22b59b1e76db7dcb8cc8ee00b6170641 100644 --- a/packages/component-user-manager/src/routes/fragmentsUsers/emails/notifications.js +++ b/packages/component-user-manager/src/routes/fragmentsUsers/emails/notifications.js @@ -13,7 +13,14 @@ const { const { getEmailCopy } = require('./emailCopy') module.exports = { - async sendNotifications({ user, baseUrl, fragment, UserModel, collection }) { + async sendNotifications({ + user, + baseUrl, + fragment, + UserModel, + collection, + reqUser, + }) { const fragmentHelper = new Fragment({ fragment }) const { title } = await fragmentHelper.getFragmentData({ handlingEditor: collection.handlingEditor, @@ -48,13 +55,16 @@ module.exports = { }) } - sendAddedToManuscriptEmail({ - email, - baseUrl, - user, - titleText, - subjectBaseText, - }) + const requestUser = await UserModel.find(reqUser) + if (requestUser.id !== user.id) { + sendAddedToManuscriptEmail({ + email, + baseUrl, + user, + titleText, + subjectBaseText, + }) + } }, } diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/post.js b/packages/component-user-manager/src/routes/fragmentsUsers/post.js index 6a7546e6f665ef1b7fb295e92c9b14a88ba8a1b8..fa0b2b2f352cd5a03988d3b537dd86b37cd87370 100644 --- a/packages/component-user-manager/src/routes/fragmentsUsers/post.js +++ b/packages/component-user-manager/src/routes/fragmentsUsers/post.js @@ -97,6 +97,7 @@ module.exports = models => async (req, res) => { baseUrl, fragment, collection, + reqUser: req.user, UserModel: models.User, }) diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index 044a3a21e91903f72a444097b665d116eac4aff2..d0b8f03d47f45aa1f8a155754152933b970757a5 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -74,7 +74,6 @@ const DashboardCard = ({ collectionId={project.id} fragmentId={version.id} modalKey={`recommend-${version.id}`} - status={project.status} /> )} <ZipFiles diff --git a/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js index fb7452baf0ba87d8e6d3edb530511df05a9a4bfa..6ad740fde0799055cbc4a8684935b0e37e8b2511 100644 --- a/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js +++ b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js @@ -9,7 +9,10 @@ import { getFormValues, reset as resetForm } from 'redux-form' import { FormItems } from '../UIComponents' import { StepOne, StepTwo, utils } from './' -import { createRecommendation } from '../../redux/recommendations' +import { + createRecommendation, + selectReviewRecommendations, +} from '../../redux/recommendations' const RecommendWizard = ({ step, @@ -40,8 +43,9 @@ const RecommendWizard = ({ export default compose( connect( - state => ({ + (state, { fragmentId }) => ({ decision: get(getFormValues('recommendation')(state), 'decision'), + reviews: selectReviewRecommendations(state, fragmentId), }), { resetForm, diff --git a/packages/components-faraday/src/components/MakeRecommendation/StepOne.js b/packages/components-faraday/src/components/MakeRecommendation/StepOne.js index b6723f9ff0a70dd03fb30668d2e8db606247cd66..1cb644c274d3bf7732c58e245090397fa33f2da6 100644 --- a/packages/components-faraday/src/components/MakeRecommendation/StepOne.js +++ b/packages/components-faraday/src/components/MakeRecommendation/StepOne.js @@ -1,5 +1,6 @@ import React from 'react' import { reduxForm } from 'redux-form' +import { isEmpty } from 'lodash' import { RadioGroup, ValidatedField, Button } from '@pubsweet/ui' import { utils } from './' @@ -7,7 +8,7 @@ import { FormItems } from '../UIComponents' const { Row, Title, RowItem, RootContainer, CustomRadioGroup } = FormItems -const StepOne = ({ hideModal, disabled, onSubmit, status }) => ( +const StepOne = ({ hideModal, disabled, onSubmit, reviews }) => ( <RootContainer> <Title>Recommendation for Next Phase</Title> <Row> @@ -21,7 +22,7 @@ const StepOne = ({ hideModal, disabled, onSubmit, status }) => ( <RadioGroup name="decision" options={ - status === 'reviewCompleted' + !isEmpty(reviews) ? utils.recommendationOptions : utils.recommendationOptions.slice(1) } diff --git a/packages/components-faraday/src/components/UIComponents/EQADecisionPage.js b/packages/components-faraday/src/components/UIComponents/EQADecisionPage.js new file mode 100644 index 0000000000000000000000000000000000000000..c1fb89f349ec68b04e977988e1e0bfea9a5fda79 --- /dev/null +++ b/packages/components-faraday/src/components/UIComponents/EQADecisionPage.js @@ -0,0 +1,231 @@ +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' +import { + compose, + withState, + lifecycle, + withHandlers, + setDisplayName, +} from 'recompose' + +import { + withModal, + ConfirmationModal, +} from 'pubsweet-component-modal/src/components' + +import { Err, Subtitle } from './FormItems' +import { parseSearchParams } from '../utils' +import { + technicalDecision, + technicalCheckFetching, +} from '../../redux/technicalCheck' + +const EQADecisionPage = ({ + params, + showEQAModal, + errorMessage, + successMessage, +}) => ( + <Root> + <Title> + Take a decision for manuscript <b>{params.customId}</b>. + </Title> + {errorMessage && <Err>{errorMessage}</Err>} + {successMessage && <Subtitle>{successMessage}</Subtitle>} + {isEmpty(errorMessage) && + isEmpty(successMessage) && ( + <ButtonContainer> + <Button onClick={showEQAModal(false)}>RETURN TO EiC</Button> + <Button onClick={showEQAModal(true)} primary> + ACCEPT + </Button> + </ButtonContainer> + )} + </Root> +) + +const DeclineModal = compose( + withState('reason', 'setReason', ''), + withHandlers({ + changeReason: ({ setReason }) => e => { + setReason(e.target.value) + }, + }), +)(({ reason, changeReason, hideModal, onConfirm, modalError }) => ( + <DeclineRoot> + <span>Return Manuscript to Editor in Chief</span> + <textarea + onChange={changeReason} + placeholder="Return reason*" + value={reason} + /> + {modalError && <ErrorMessage>{modalError}</ErrorMessage>} + <ButtonContainer data-test="eqa-buttons"> + <Button onClick={hideModal}>Cancel</Button> + <Button disabled={!reason} onClick={onConfirm(reason)} primary> + Send + </Button> + </ButtonContainer> + </DeclineRoot> +)) + +const ModalComponent = ({ type, ...rest }) => + type === 'decline' ? ( + <DeclineModal {...rest} /> + ) : ( + <ConfirmationModal {...rest} /> + ) + +export default compose( + setDisplayName('EQA Decision page'), + connect( + state => ({ + isFetching: technicalCheckFetching(state), + }), + { technicalDecision }, + ), + withModal(({ isFetching }) => ({ + isFetching, + modalComponent: ModalComponent, + })), + withState('params', 'setParams', { + token: null, + customId: null, + collectionId: null, + }), + withState('successMessage', 'setSuccess', ''), + lifecycle({ + componentDidMount() { + const { location, setParams } = this.props + const { customId, collectionId, token } = parseSearchParams( + location.search, + ) + setParams({ customId, collectionId, token }) + }, + }), + withHandlers({ + showEQAModal: ({ + showModal, + hideModal, + setSuccess, + setModalError, + technicalDecision, + params: { collectionId, token }, + }) => decision => () => { + const acceptConfig = { + title: `Are you sure you want to accept this EQA package?`, + onConfirm: () => { + technicalDecision({ + step: 'eqa', + agree: decision, + collectionId, + token, + }).then(() => { + setSuccess( + `Manuscript accepted. Thank you for your technical check!`, + ) + hideModal() + }, setModalError) + }, + onCancel: hideModal, + } + const declineConfig = { + type: 'decline', + title: 'Return Manuscript to Editor in Chief', + onConfirm: reason => () => { + technicalDecision({ + step: 'eqa', + agree: decision, + comments: reason, + collectionId, + token, + }).then(() => { + setSuccess( + `Manuscript returned with comments. An email has been sent to Editor In Chief. Thank you for your technical check!`, + ) + hideModal() + }, setModalError) + }, + } + + const cfg = decision ? acceptConfig : declineConfig + showModal(cfg) + }, + }), +)(EQADecisionPage) + +// #region styles +const Root = styled.div` + align-items: center; + color: ${th('colorText')}; + display: flex; + flex-direction: column; + justify-content: flex-start; + margin: 0 auto; + text-align: center; + width: 70vw; + + a { + color: ${th('colorText')}; + } +` + +const Title = styled.div` + color: ${th('colorPrimary')}; + font-size: ${th('fontSizeHeading5')}; + font-family: ${th('fontHeading')}; + margin: 10px auto; +` + +const ButtonContainer = styled.div` + align-items: center; + display: flex; + justify-content: space-around; + padding: calc(${th('gridUnit')} / 2); + width: calc(${th('gridUnit')} * 15); +` +const ErrorMessage = styled.div` + color: ${th('colorError')}; + margin: ${th('subGridUnit')}; + text-align: center; +` +const DeclineRoot = styled.div` + align-items: center; + background-color: ${th('backgroundColor')}; + display: flex; + flex-direction: column; + height: calc(${th('gridUnit')} * 13); + justify-content: space-between; + padding: calc(${th('subGridUnit')} * 7); + width: calc(${th('gridUnit')} * 24); + + & span { + color: ${th('colorPrimary')}; + font-size: ${th('fontSizeHeading5')}; + font-family: ${th('fontHeading')}; + margin-bottom: ${th('gridUnit')}; + } + + & textarea { + height: 100%; + padding: calc(${th('subGridUnit')} * 2); + width: 100%; + } + + & textarea:focus, + & textarea:active { + outline: none; + } + + & div { + display: flex; + justify-content: space-evenly; + margin: ${th('gridUnit')} auto 0; + width: 100%; + } +` +// #endregion diff --git a/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js b/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js index 6aaa02bb30d2245e08ac88c503cbc111651d7c63..165361b6ff2853f62f96dbb9082870a66ed87781 100644 --- a/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js +++ b/packages/components-faraday/src/components/UIComponents/EQSDecisionPage.js @@ -21,7 +21,7 @@ import { Err, Subtitle } from './FormItems' import { parseSearchParams } from '../utils' import { technicalDecision, - technicalCheckFetcing, + technicalCheckFetching, } from '../../redux/technicalCheck' const EQSDecisionPage = ({ @@ -52,7 +52,7 @@ export default compose( setDisplayName('EQS Decision page'), connect( state => ({ - isFetching: technicalCheckFetcing(state), + isFetching: technicalCheckFetching(state), }), { technicalDecision }, ), diff --git a/packages/components-faraday/src/components/UIComponents/index.js b/packages/components-faraday/src/components/UIComponents/index.js index a619f97e10cfcf1546e5428b5773ed6e6530e336..ff001b8fb3a18548136a1a88e64816e3cde7a4b8 100644 --- a/packages/components-faraday/src/components/UIComponents/index.js +++ b/packages/components-faraday/src/components/UIComponents/index.js @@ -8,5 +8,6 @@ export { default as InfoPage } from './InfoPage' export { default as ErrorPage } from './ErrorPage' export { default as DateParser } from './DateParser' export { default as EQSDecisionPage } from './EQSDecisionPage' +export { default as EQADecisionPage } from './EQADecisionPage' export { default as ConfirmationPage } from './ConfirmationPage' export { default as BreadcrumbsHeader } from './BreadcrumbsHeader' diff --git a/packages/components-faraday/src/redux/technicalCheck.js b/packages/components-faraday/src/redux/technicalCheck.js index 8cb3a074bf9eed5220f56f7f476a7510cda8dc57..5c8d7b9c2e5125ecfa487aab80cab12234a3c361 100644 --- a/packages/components-faraday/src/redux/technicalCheck.js +++ b/packages/components-faraday/src/redux/technicalCheck.js @@ -22,6 +22,7 @@ export const technicalDecision = ({ step, agree, token, + comments, collectionId, }) => dispatch => { dispatch(decisionRequest()) @@ -29,19 +30,25 @@ export const technicalDecision = ({ step, token, agree, + comments, }).then( r => { dispatch(decisionSuccess()) return r }, err => { - dispatch(decisionError(err)) - throw err + const errorMessage = get( + JSON.parse(err.response), + 'error', + 'Oops! Something went wrong!', + ) + dispatch(decisionError(errorMessage)) + throw errorMessage }, ) } -export const technicalCheckFetcing = state => +export const technicalCheckFetching = state => get(state, 'technicalCheck.fetching', false) export default (state = {}, action = {}) => { diff --git a/packages/xpub-faraday/app/routes.js b/packages/xpub-faraday/app/routes.js index 05e3106ebac869d37c6b1401cf8764def29ab516..e3cb4ba8b9a232317471c34f98ac6f73b3ce92cd 100644 --- a/packages/xpub-faraday/app/routes.js +++ b/packages/xpub-faraday/app/routes.js @@ -16,6 +16,7 @@ import { InfoPage, ErrorPage, EQSDecisionPage, + EQADecisionPage, ConfirmationPage, } from 'pubsweet-components-faraday/src/components/UIComponents/' import { @@ -120,6 +121,7 @@ const Routes = () => ( path="/projects/:project/versions/:version/details" /> <Route component={EQSDecisionPage} exact path="/eqs-decision" /> + <Route component={EQADecisionPage} exact path="/eqa-decision" /> <Route component={ErrorPage} exact path="/error-page" /> <Route component={InfoPage} exact path="/info-page" /> <Route component={NotFound} /> diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js index a39537adc4125ddc1e2327c0b3c7aeeb84d1064d..819278c83b59ebec49ad8628363421503c649948 100644 --- a/packages/xpub-faraday/config/default.js +++ b/packages/xpub-faraday/config/default.js @@ -97,6 +97,9 @@ module.exports = { 'eqs-decision': { url: process.env.PUBSWEET_EQS_DECISION || '/eqs-decision', }, + 'eqa-decision': { + url: process.env.PUBSWEET_EQA_DECISION || '/eqa-decision', + }, unsubscribe: { url: process.env.PUBSWEET_UNSUBSCRIBE_URL || '/unsubscribe', }, diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index 4a96afcddc925807a78fa9495b8be0aa53aedec8..25f2501d38bb6ffaa7619b683bcb3825e28e0883 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -13,6 +13,7 @@ module.exports = { handlingEditor: Joi.object(), technicalChecks: Joi.object({ token: Joi.string(), + hasEQA: Joi.boolean(), }), }, fragment: [