diff --git a/packages/component-email/src/routes/emails/helpers.js b/packages/component-email/src/routes/emails/helpers.js index 8f484b0a50207f09e5f8bbb4810ff23516bf40b2..35395e3da3bb1fac36c7d2006ca49122f955cb3e 100644 --- a/packages/component-email/src/routes/emails/helpers.js +++ b/packages/component-email/src/routes/emails/helpers.js @@ -4,19 +4,13 @@ const { services } = require('pubsweet-component-helper-service') const { getEmailCopy } = require('./emailCopy') const confirmSignUp = config.get('confirm-signup.url') -const { name: journalName, staffEmail } = config.get('journal') module.exports = { sendNewUserEmail: ({ email, role }) => { email.content.subject = 'Confirm your account' - let emailType - if (role === 'Handling Editor') { - emailType = 'he-added-by-admin' - email.fromEmail = `${journalName} <${staffEmail}>` - } else { - emailType = 'user-added-by-admin' - } + const emailType = + role === 'Handling Editor' ? 'he-added-by-admin' : 'user-added-by-admin' const { html, text } = email.getNotificationBody({ emailBodyProps: getEmailCopy({ diff --git a/packages/component-email/src/routes/emails/notifications.js b/packages/component-email/src/routes/emails/notifications.js index 84a1b0a863d3e015707391a99062896268dc4dd7..5fe6fc1183bef9386e5b8f811a0ee6084e1387b3 100644 --- a/packages/component-email/src/routes/emails/notifications.js +++ b/packages/component-email/src/routes/emails/notifications.js @@ -2,7 +2,7 @@ const config = require('config') const unsubscribeSlug = config.get('unsubscribe.url') const resetPath = config.get('invite-reset-password.url') -const staffEmail = config.get('journal.staffEmail') +const { staffEmail, name: journalName } = config.get('journal') const Email = require('@pubsweet/component-email-templating') const { services } = require('pubsweet-component-helper-service') @@ -12,7 +12,7 @@ module.exports = { async sendNotifications({ user, baseUrl, role }) { const email = new Email({ type: 'user', - fromEmail: `Hindawi <${staffEmail}>`, + fromEmail: `${journalName} <${staffEmail}>`, toUser: { email: user.email, name: `${user.lastName}`, diff --git a/packages/component-faraday-ui/src/PublonsTable.js b/packages/component-faraday-ui/src/PublonsTable.js index 4b7a53511d9312f03b61f77d11b52a83e8f5b85d..ce9f549473a84b3e60bda962f46054e93cefd1ef 100644 --- a/packages/component-faraday-ui/src/PublonsTable.js +++ b/packages/component-faraday-ui/src/PublonsTable.js @@ -58,8 +58,8 @@ const TableView = ({ <HiddenCell> <OpenModal confirmText="Invite" - isFetching={isFetching} onConfirm={modalProps => onInviteReviewer(reviewer, modalProps)} + publonsFetching={isFetching} setFetching={setFetching} title="Send invitation to review?" > diff --git a/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js b/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js index 1bd5ca285bd9e16c2af76402abe6689c3bf2738a..105b76fc04c6a4046c794398157ee770135ad538 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js +++ b/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js @@ -18,7 +18,8 @@ const SubmittedReportsNumberForAuthorReviews = ({ reports }) => ( const AuthorReviews = ({ invitations, journal, reports, fragment }) => reports.length > 0 && ( <ContextualBox - label="Review Reports" + label="Reviewer Reports" + mb={2} rightChildren={ <SubmittedReportsNumberForAuthorReviews reports={reports.length} /> } diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js index 40affb63b76e5970b505ea5300633084b931d141..d4a4310b2e39b974037596a603de4ecfac7e7354 100644 --- a/packages/component-helper-service/src/services/Collection.js +++ b/packages/component-helper-service/src/services/Collection.js @@ -68,9 +68,10 @@ class Collection { handlingEditor.hasAnswer = true handlingEditor.isAccepted = isAccepted handlingEditor.respondedOn = Date.now() - let status - isAccepted ? (status = 'heAssigned') : (status = 'submitted') - await this.updateStatus({ newStatus: status }) + + const newStatus = isAccepted ? 'heAssigned' : 'submitted' + + await this.updateStatus({ newStatus }) } async updateStatusByNumberOfReviewers({ invitations }) { diff --git a/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js b/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js index da8d7c94bb43f5ca550cb12e7f609468aed6c190..11a81820196eb1e7d4ac0048892def94fa91b031 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js +++ b/packages/component-invite/src/routes/collectionsInvitations/emails/helpers.js @@ -4,7 +4,6 @@ const { services } = require('pubsweet-component-helper-service') const { getEmailCopy } = require('./emailCopy') const unsubscribeSlug = config.get('unsubscribe.url') -const { name: journalName, staffEmail } = config.get('journal') module.exports = { sendInvitedHEEmail: ({ @@ -24,7 +23,6 @@ module.exports = { ? `${customId}: Editor invitation cancelled` : `${customId}: Invitation to edit a manuscript` - email.fromEmail = `${journalName} <${staffEmail}>` email.content.unsubscribeLink = services.createUrl( baseUrl, unsubscribeSlug, diff --git a/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js b/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js index 2cf01e436af71c6273d998a741e08205e40862c0..65e1375189adc5fc06576082ad8c8b206ae35dc0 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js +++ b/packages/component-invite/src/routes/collectionsInvitations/emails/notifications.js @@ -1,6 +1,9 @@ +const config = require('config') const { last } = require('lodash') const Email = require('@pubsweet/component-email-templating') +const { name: journalName, staffEmail } = config.get('journal') + const { User, services, @@ -40,6 +43,7 @@ module.exports = { const email = new Email({ type: 'user', + fromEmail: `${journalName} <${staffEmail}>`, content: { signatureName: eicName, ctaLink: services.createUrl( diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js b/packages/component-manuscript-manager/src/notifications/emailCopy.js similarity index 90% rename from packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js rename to packages/component-manuscript-manager/src/notifications/emailCopy.js index 8fadac625272f7f5a3d200d11da0af3e5a1d1270..3aa154fcdd356c251d191cf3d38b02778e1379cf 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/emailCopy.js +++ b/packages/component-manuscript-manager/src/notifications/emailCopy.js @@ -1,4 +1,5 @@ const config = require('config') +const { upperFirst } = require('lodash') const staffEmail = config.get('journal.staffEmail') const journalName = config.get('journal.name') @@ -42,6 +43,8 @@ const getEmailCopy = ({ Thank you for handling this manuscript on behalf of ${journalName}.` break case 'he-manuscript-published': + hasIntro = false + hasSignature = false paragraph = `${targetUserName} has confirmed your decision to accept ${titleText}.<br/><br/> No further action is required at this time. To review this decision, please visit the manuscript details.<br/><br/> Thank you for handling this manuscript on behalf of ${journalName}.` @@ -106,7 +109,8 @@ const getEmailCopy = ({ hasLink = false hasIntro = false hasSignature = false - paragraph = `${titleText} has been accepted for publication by ${eicName}. <br/><br/> + paragraph = `${upperFirst(titleText)} has been accepted + for publication by ${eicName}.<br/><br/> Please complete QA screening so that manuscript can be sent to production.` break case 'authors-manuscript-rejected-before-review': @@ -115,6 +119,18 @@ const getEmailCopy = ({ Thank you for your submission, and please do consider submitting again in the future.` hasLink = false break + case 'eic-manuscript-accepted-by-eqs': + hasIntro = false + hasSignature = false + paragraph = `A new ${titleText} has been submitted to ${journalName}.<br/><br/> + To begin the review process, please visit the manuscript details page.` + break + case 'eic-manuscript-returned-by-eqa': + hasIntro = false + hasSignature = false + 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.`) } diff --git a/packages/component-manuscript-manager/src/notifications/helpers.js b/packages/component-manuscript-manager/src/notifications/helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..bb1302a2fb093c93316475c47e9532093ae9cde0 --- /dev/null +++ b/packages/component-manuscript-manager/src/notifications/helpers.js @@ -0,0 +1,44 @@ +const { get } = require('lodash') + +module.exports = { + getPrivateNoteTextForAuthor: ({ newRecommendation }) => { + const authorNote = newRecommendation.comments.find(comm => comm.public) + const content = get(authorNote, 'content') + + return content ? `Reason & Details: "${content}"` : '' + }, + getHEComments: ({ heRecommendation }) => { + const heComments = get(heRecommendation, 'comments', []) + + if (heComments.length === 0) return + + const publicComment = heComments.find(comm => comm.public) + + const content = get(publicComment, 'content') + if (!content) { + throw new Error('a public comment cannot be without content') + } + + return `Please find our editorial comments below.<br/> "${content}"` + }, + getEmailTypeByRecommendationForAuthors: ({ + recommendation, + hasPeerReview, + }) => { + let emailType + switch (recommendation) { + case 'publish': + emailType = 'author-manuscript-published' + break + case 'reject': + emailType = hasPeerReview + ? 'author-manuscript-rejected' + : 'authors-manuscript-rejected-before-review' + break + default: + throw new Error(`Undefined recommendation: ${recommendation}`) + } + + return emailType + }, +} diff --git a/packages/component-manuscript-manager/src/notifications/notification.js b/packages/component-manuscript-manager/src/notifications/notification.js new file mode 100644 index 0000000000000000000000000000000000000000..dfb831c3585207a0aacbb7b0f20f4e8f5a20e4eb --- /dev/null +++ b/packages/component-manuscript-manager/src/notifications/notification.js @@ -0,0 +1,652 @@ +const config = require('config') +const { get, isEmpty, chain } = require('lodash') +const Email = require('@pubsweet/component-email-templating') +const { + User, + services, + Fragment, + Collection, +} = require('pubsweet-component-helper-service') + +const helpers = require('./helpers') +const { getEmailCopy } = require('./emailCopy') + +const { name: journalName, staffEmail } = config.get('journal') +const unsubscribeSlug = config.get('unsubscribe.url') + +class Notification { + constructor({ + baseUrl = '', + fragment = {}, + UserModel = {}, + collection = {}, + newRecommendation = {}, + }) { + this.baseUrl = baseUrl + this.fragment = fragment + this.UserModel = UserModel + this.collection = collection + this.newRecommendation = newRecommendation + } + + async notifyHEWhenReviewerSubmitsReport(reviewerLastName) { + const { eicName, titleText } = await this.getNotificationProperties() + + const handlingEditorId = get(this.collection, 'handlingEditor.id') + const heUser = await this.UserModel.find(handlingEditorId) + + const email = new Email({ + type: 'user', + toUser: { + email: heUser.email, + name: heUser.lastName, + }, + fromEmail: `${eicName} <${staffEmail}>`, + content: { + signatureName: eicName, + ctaText: 'MANUSCRIPT DETAILS', + subject: `${this.collection.customId}: A review has been submitted`, + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: heUser.id, + token: heUser.accessTokens.unsubscribe, + }), + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + }, + }) + + const emailBodyProps = getEmailCopy({ + emailType: 'he-review-submitted', + titleText, + targetUserName: reviewerLastName, + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + } + + // EiC decided to either publish or reject the manuscript + async notifyHEWhenEiCMakesDecision() { + const { + eicName, + titleText, + recommendation, + } = await this.getNotificationProperties() + + const handlingEditorId = get(this.collection, 'handlingEditor.id') + const heUser = await this.UserModel.find(handlingEditorId) + + const email = new Email({ + type: 'user', + toUser: { + email: heUser.email, + }, + fromEmail: `${journalName} <${staffEmail}>`, + content: { + ctaText: 'MANUSCRIPT DETAILS', + subject: `${this.collection.customId}: Editorial decision confirmed`, + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: heUser.id, + token: heUser.accessTokens.unsubscribe, + }), + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + }, + }) + + const emailBodyProps = getEmailCopy({ + titleText, + targetUserName: eicName, + emailType: + recommendation === 'publish' + ? 'he-manuscript-published' + : 'he-manuscript-rejected', + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + } + + async notifyHEWhenEiCReturnsToHE() { + const { eicName, titleText } = await this.getNotificationProperties() + + const handlingEditorId = get(this.collection, 'handlingEditor.id') + const heUser = await this.UserModel.find(handlingEditorId) + + const email = new Email({ + type: 'user', + toUser: { + email: heUser.email, + name: heUser.lastName, + }, + fromEmail: `${eicName} <${staffEmail}>`, + content: { + subject: `${ + this.collection.customId + }: Editorial decision returned with comments`, + signatureName: eicName, + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: heUser.id, + token: heUser.accessTokens.unsubscribe, + }), + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + }, + }) + + const eicComments = chain(this.newRecommendation) + .get('comments') + .find(comm => !comm.public) + .get('content') + .value() + + const emailBodyProps = getEmailCopy({ + titleText, + comments: eicComments, + targetUserName: eicName, + emailType: 'he-manuscript-return-with-comments', + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + } + + async notifyEAWhenEiCMakesFinalDecision() { + const { eicName, titleText } = await this.getNotificationProperties() + const subjectBaseText = `${this.collection.customId}: Manuscript` + + const email = new Email({ + type: 'system', + toUser: { + email: staffEmail, + }, + fromEmail: `${journalName} <${staffEmail}>`, + content: { + subject: `${subjectBaseText} decision finalized`, + unsubscribeLink: this.baseUrl, + }, + }) + + const emailBodyProps = getEmailCopy({ + eicName, + titleText, + emailType: 'eqa-manuscript-published', + }) + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + } + + async notifyEAWhenEiCRequestsEQAApproval() { + const { eicName } = await this.getNotificationProperties() + const subjectBaseText = `${this.collection.customId}: Manuscript` + + const email = new Email({ + type: 'system', + toUser: { + email: staffEmail, + }, + fromEmail: `${journalName} <${staffEmail}>`, + content: { + subject: `${subjectBaseText} Request for EQA approval`, + unsubscribeLink: this.baseUrl, + ctaText: 'MAKE DECISION', + ctaLink: services.createUrl( + this.baseUrl, + config.get('eqa-decision.url'), + { + collectionId: this.collection.id, + customId: this.collection.customId, + token: this.collection.technicalChecks.token, + }, + ), + }, + }) + + const emailBodyProps = getEmailCopy({ + eicName, + customId: this.collection.customId, + emailType: 'eqa-manuscript-request-for-approval', + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + } + + async notifyAuthorsWhenEiCMakesDecision() { + const { + eicName, + titleText, + activeAuthors, + recommendation, + parsedFragment, + } = await this.getNotificationProperties() + + const subjectOpts = { + publish: `${this.collection.customId}: Manuscript accepted`, + reject: `${this.collection.customId}: Manuscript rejected`, + } + const subject = subjectOpts[recommendation] + + if (isEmpty(subject)) { + throw new Error(`Undefined recommendation: ${recommendation}`) + } + + const hasPeerReview = !isEmpty(this.collection.handlingEditor) + const emailType = helpers.getEmailTypeByRecommendationForAuthors({ + recommendation, + hasPeerReview, + }) + const comments = hasPeerReview + ? helpers.getHEComments({ + heRecommendation: parsedFragment.heRecommendation, + }) + : this.newRecommendation.comments[0].content + + activeAuthors.forEach(async author => { + const email = new Email({ + type: 'user', + toUser: { + email: author.email, + name: author.lastName, + }, + fromEmail: `${eicName} <${staffEmail}>`, + content: { + signatureName: eicName, + ctaText: 'MANUSCRIPT DETAILS', + subject, + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: author.id, + token: author.accessTokens.unsubscribe, + }), + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + signatureJournal: journalName, + }, + }) + + const emailBodyProps = getEmailCopy({ + titleText, + emailType, + comments, + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + }) + } + + async notifyReviewersWhenEiCMakesDecision() { + const { + eicName, + titleText, + recommendation, + fragmentHelper, + } = await this.getNotificationProperties() + + const emailType = + recommendation === 'publish' + ? 'submitted-reviewers-after-publish' + : 'submitted-reviewers-after-reject' + + const subject = + recommendation === 'publish' + ? 'A manuscript you reviewed has been accepted' + : 'A manuscript you reviewed has been rejected' + + const reviewers = await fragmentHelper.getReviewers({ + UserModel: this.UserModel, + type: 'submitted', + }) + + const emailBodyProps = getEmailCopy({ + emailType, + titleText, + }) + + reviewers.forEach(reviewer => { + const email = new Email({ + type: 'user', + toUser: { + email: reviewer.email, + name: reviewer.lastName, + }, + fromEmail: `${eicName} <${staffEmail}>`, + content: { + signatureName: eicName, + ctaText: 'MANUSCRIPT DETAILS', + subject: `${this.collection.customId}: ${subject}`, + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: reviewer.id, + token: reviewer.accessTokens.unsubscribe, + }), + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + signatureJournal: journalName, + }, + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + }) + } + + async notifySAWhenHERequestsRevision() { + const { + eicName, + submittingAuthor, + parsedFragment: { title }, + } = await this.getNotificationProperties() + + const authorNoteText = helpers.getPrivateNoteTextForAuthor({ + newRecommendation: this.newRecommendation, + }) + + const signatureName = get(this.collection, 'handlingEditor.name', eicName) + + const email = new Email({ + type: 'user', + toUser: { + email: submittingAuthor.email, + name: submittingAuthor.lastName, + }, + fromEmail: `${signatureName} <${staffEmail}>`, + content: { + signatureName, + ctaText: 'MANUSCRIPT DETAILS', + signatureJournal: journalName, + subject: `${this.collection.customId}: Revision requested`, + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: submittingAuthor.id, + token: submittingAuthor.accessTokens.unsubscribe, + }), + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + }, + }) + + const emailBodyProps = getEmailCopy({ + emailType: 'author-request-to-revision', + titleText: `your submission "${title}" to ${journalName}`, + comments: authorNoteText, + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + } + + async notifyReviewersWhenHEMakesRecommendation() { + const { + eicName, + titleText, + fragmentHelper, + } = await this.getNotificationProperties() + + const signatureName = get(this.collection, 'handlingEditor.name', eicName) + + const acceptedReviewers = await fragmentHelper.getReviewers({ + UserModel: this.UserModel, + type: 'accepted', + }) + const acceptedReviewersEmailBody = getEmailCopy({ + emailType: 'accepted-reviewers-after-recommendation', + titleText, + }) + + const pendingReviewers = await fragmentHelper.getReviewers({ + UserModel: this.UserModel, + type: 'pending', + }) + const pendingReviewersEmailBody = getEmailCopy({ + emailType: 'pending-reviewers-after-recommendation', + titleText, + }) + + const buildSendEmailFunction = emailBodyProps => reviewer => { + const email = new Email({ + type: 'user', + toUser: { + email: reviewer.email, + name: reviewer.lastName, + }, + fromEmail: `${signatureName} <${staffEmail}>`, + content: { + signatureName, + ctaText: 'MANUSCRIPT DETAILS', + subject: `${this.collection.customId}: Review no longer required`, + unsubscribeLink: services.createUrl(this.baseUrl, unsubscribeSlug, { + id: reviewer.id, + token: reviewer.accessTokens.unsubscribe, + }), + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + signatureJournal: journalName, + }, + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + } + + acceptedReviewers.forEach( + buildSendEmailFunction(acceptedReviewersEmailBody), + ) + pendingReviewers.forEach(buildSendEmailFunction(pendingReviewersEmailBody)) + } + + async notifyEiCWhenHEMakesRecommendation() { + const { + eicName, + titleText, + recommendation, + } = await this.getNotificationProperties() + + let emailType, subject + switch (recommendation) { + case 'minor': + case 'major': + emailType = 'eic-request-revision-from-he' + subject = `${this.collection.customId}: Revision requested` + break + case 'publish': + emailType = 'eic-recommend-to-publish-from-he' + subject = `${this.collection.customId}: Recommendation to publish` + break + case 'reject': + emailType = 'eic-recommend-to-reject-from-he' + subject = `${this.collection.customId}: Recommendation to reject` + break + default: + throw new Error(`undefined recommendation: ${recommendation} `) + } + + const privateNote = this.newRecommendation.comments.find( + comm => !comm.public, + ) + const content = get(privateNote, 'content') + const comments = content + ? `The editor provided the following comments: "${content}"` + : '' + + const collHelper = new Collection({ collection: this.collection }) + const targetUserName = collHelper.getHELastName() + + const userHelper = new User({ UserModel: this.UserModel }) + const editors = await userHelper.getEditorsInChief() + + const emailBodyProps = getEmailCopy({ + comments, + emailType, + titleText, + targetUserName, + }) + + editors.forEach(eic => { + const email = new Email({ + type: 'user', + toUser: { + email: eic.email, + name: `${eic.firstName} ${eic.lastName}`, + }, + fromEmail: `${journalName} <${staffEmail}>`, + content: { + signatureName: eicName, + subject, + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: this.baseUrl, + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + }, + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + }) + } + + async getNotificationProperties() { + const fragmentHelper = new Fragment({ fragment: this.fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: this.collection.handlingEditor, + }) + const { + submittingAuthor, + activeAuthors, + } = await fragmentHelper.getAuthorData({ + UserModel: this.UserModel, + }) + + const userHelper = new User({ UserModel: this.UserModel }) + const eicName = await userHelper.getEiCName() + + const titleText = `the manuscript titled "${parsedFragment.title}" by ${ + submittingAuthor.firstName + } ${submittingAuthor.lastName}` + + const { recommendation, recommendationType } = this.newRecommendation + + return { + recommendation, + recommendationType, + eicName, + titleText, + submittingAuthor, + activeAuthors, + parsedFragment, + fragmentHelper, + } + } + + async notifyEiCWhenEQSAcceptsManuscript() { + const { submittingAuthor } = await this.getNotificationProperties() + + const userHelper = new User({ UserModel: this.UserModel }) + const editors = await userHelper.getEditorsInChief() + const fragmentHelper = new Fragment({ fragment: this.fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({}) + + const emailBodyProps = getEmailCopy({ + emailType: 'eic-manuscript-accepted-by-eqs', + titleText: `manuscript titled "${parsedFragment.title}" by ${ + submittingAuthor.firstName + } ${submittingAuthor.lastName}`, + }) + + editors.forEach(eic => { + const email = new Email({ + type: 'system', + toUser: { + email: eic.email, + }, + fromEmail: `${journalName} <${staffEmail}>`, + content: { + subject: `${this.collection.customId}: New manuscript submitted`, + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: this.baseUrl, + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + }, + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + }) + } + + async notifyEiCWhenEQARejectsManuscript(comments) { + const { titleText } = await this.getNotificationProperties() + + const userHelper = new User({ UserModel: this.UserModel }) + const editors = await userHelper.getEditorsInChief() + + const emailBodyProps = getEmailCopy({ + comments, + titleText, + emailType: 'eic-manuscript-returned-by-eqa', + }) + + editors.forEach(eic => { + const email = new Email({ + type: 'system', + toUser: { + email: eic.email, + }, + fromEmail: `${journalName} <${staffEmail}>`, + content: { + subject: `${ + this.collection.customId + }: Manuscript returned with comments`, + ctaText: 'MANUSCRIPT DETAILS', + unsubscribeLink: this.baseUrl, + ctaLink: services.createUrl( + this.baseUrl, + `/projects/${this.collection.id}/versions/${ + this.fragment.id + }/details`, + ), + }, + }) + + const { html, text } = email.getNotificationBody({ emailBodyProps }) + email.sendEmail({ html, text }) + }) + } +} + +module.exports = Notification diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/helpers.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/helpers.js deleted file mode 100644 index c03dedb12bd798664f0d1855fcd4af9cb2f345fb..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/helpers.js +++ /dev/null @@ -1,401 +0,0 @@ -const config = require('config') -const { get, chain } = require('lodash') - -const { services } = require('pubsweet-component-helper-service') -const { getEmailCopy } = require('./emailCopy') - -const unsubscribeSlug = config.get('unsubscribe.url') -const journalName = config.get('journal.name') - -module.exports = { - sendReviewersEmail: async ({ email, baseUrl, reviewers }) => { - reviewers.forEach(reviewer => { - email.toUser = { - email: reviewer.email, - name: `${reviewer.lastName}`, - } - email.content.unsubscribeLink = services.createUrl( - baseUrl, - unsubscribeSlug, - { - id: reviewer.id, - token: reviewer.accessTokens.unsubscribe, - }, - ) - const { html, text } = email.getNotificationBody({ - emailBodyProps: { - paragraph: reviewer.paragraph, - hasLink: reviewer.hasLink, - hasIntro: reviewer.hasIntro, - hasSignature: reviewer.hasSignature, - }, - }) - email.sendEmail({ html, text }) - }) - }, - getSubmittedReviewers: async ({ - UserModel, - titleText, - fragmentHelper, - recommendation, - }) => { - const emailType = - recommendation === 'publish' - ? 'submitted-reviewers-after-publish' - : 'submitted-reviewers-after-reject' - - const reviewers = (await fragmentHelper.getReviewers({ - UserModel, - type: 'submitted', - })).map(rev => ({ - ...rev, - emailType, - ...getEmailCopy({ - emailType, - titleText, - }), - })) - - return reviewers - }, - getNoResponseReviewers: async ({ fragmentHelper, UserModel, titleText }) => { - const acceptedReviewers = (await fragmentHelper.getReviewers({ - UserModel, - type: 'accepted', - })).map(rev => ({ - ...rev, - ...getEmailCopy({ - emailType: 'accepted-reviewers-after-recommendation', - titleText, - }), - })) - - const pendingReviewers = (await fragmentHelper.getReviewers({ - UserModel, - type: 'pending', - })).map(rev => ({ - ...rev, - ...getEmailCopy({ - emailType: 'pending-reviewers-after-recommendation', - titleText, - }), - })) - - const reviewers = [...acceptedReviewers, ...pendingReviewers] - - return reviewers - }, - updateEmailContentForReviewers: ({ - email, - customId, - signatureName, - recommendation, - }) => { - const subject = - recommendation === 'publish' - ? 'A manuscript you reviewed has been accepted' - : 'A manuscript you reviewed has been rejected' - - email.content.subject = `${customId}: ${subject}` - email.content.signatureName = signatureName - email.content.signatureJournal = journalName - return email - }, - sendHandlingEditorEmail: ({ - email, - comments, - emailType, - titleText, - targetUserName, - }) => { - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - emailType, - titleText, - comments, - targetUserName, - }), - }) - email.sendEmail({ html, text }) - }, - updateEmailContentForHE: ({ - email, - baseUrl, - eicName, - customId, - heLastName, - handlingEditor, - recommendation, - recommendationType, - }) => { - if (recommendationType === 'review') { - email.content.subject = `${customId}: A review has been submitted` - } else { - switch (recommendation) { - case 'return-to-handling-editor': - email.content.subject = `${customId}: Editorial decision returned with comments` - break - case 'publish': - case 'reject': - email.content.subject = `${customId}: Editorial decision confirmed` - break - default: - throw new Error(`Undefined recommendation: ${recommendation}`) - } - } - - email.toUser = { - email: handlingEditor.email, - name: heLastName, - } - email.content.unsubscribeLink = services.createUrl( - baseUrl, - unsubscribeSlug, - { - id: handlingEditor.id, - token: handlingEditor.accessTokens.unsubscribe, - }, - ) - - email.content.signatureName = eicName - delete email.content.signatureJournal - - return email - }, - getEmailTypeByRecommendationForHE: ({ - recommendation, - recommendationType, - }) => { - let emailType - if (recommendationType === 'review') { - emailType = 'he-review-submitted' - } else { - switch (recommendation) { - case 'return-to-handling-editor': - emailType = 'he-manuscript-return-with-comments' - break - case 'publish': - emailType = 'he-manuscript-published' - break - case 'reject': - emailType = 'he-manuscript-rejected' - break - default: - throw new Error(`undefined recommendation: ${recommendation} `) - } - } - - return emailType - }, - getEiCCommentsForHE: ({ newRecommendation }) => { - const eicComments = chain(newRecommendation) - .get('comments') - .find(comm => !comm.public) - .get('content') - .value() - - return eicComments - }, - sendEiCsEmail: async ({ - email, - customId, - titleText, - userHelper, - targetUserName, - recommendation: { recommendation, comments: recComments = [] }, - }) => { - let emailType - - switch (recommendation) { - case 'minor': - case 'major': - emailType = 'eic-request-revision-from-he' - email.content.subject = `${customId}: Revision requested` - break - case 'publish': - emailType = 'eic-recommend-to-publish-from-he' - email.content.subject = `${customId}: Recommendation to publish` - break - case 'reject': - emailType = 'eic-recommend-to-reject-from-he' - email.content.subject = `${customId}: Recommendation to reject` - break - default: - throw new Error(`undefined recommendation: ${recommendation} `) - } - delete email.content.signatureJournal - - const privateNote = recComments.find(comm => !comm.public) - const content = get(privateNote, 'content') - const comments = content - ? `The editor provided the following comments: "${content}"` - : '' - - const editors = (await userHelper.getEditorsInChief()).map(eic => ({ - ...eic, - ...getEmailCopy({ - comments, - emailType, - titleText, - targetUserName, - }), - })) - - editors.forEach(eic => { - email.toUser = { - email: eic.email, - name: `${eic.firstName} ${eic.lastName}`, - } - const { html, text } = email.getNotificationBody({ - emailBodyProps: { - paragraph: eic.paragraph, - hasLink: eic.hasLink, - hasIntro: eic.hasIntro, - hasSignature: eic.hasSignature, - }, - }) - email.sendEmail({ html, text }) - }) - }, - sendAuthorsEmail: async ({ email, authors, baseUrl }) => { - authors.forEach(author => { - email.toUser = { - email: author.email, - name: `${author.lastName}`, - } - email.content.unsubscribeLink = services.createUrl( - baseUrl, - unsubscribeSlug, - { - id: author.id, - token: author.accessTokens.unsubscribe, - }, - ) - const { html, text } = email.getNotificationBody({ - emailBodyProps: { - paragraph: author.paragraph, - hasLink: author.hasLink, - hasIntro: author.hasIntro, - hasSignature: author.hasSignature, - }, - }) - email.sendEmail({ html, text }) - }) - }, - getSubmittingAuthor: ({ - title, - journalName, - authorNoteText, - submittingAuthor, - }) => { - const author = { - ...submittingAuthor, - ...getEmailCopy({ - emailType: 'author-request-to-revision', - titleText: `your submission "${title}" to ${journalName}`, - comments: authorNoteText, - }), - } - - return author - }, - getPrivateNoteTextForAuthor: ({ newRecommendation }) => { - const authorNote = newRecommendation.comments.find(comm => comm.public) - const content = get(authorNote, 'content') - const authorNoteText = content ? `Reason & Details: "${content}"` : '' - - return authorNoteText - }, - updateEmailContentForSA: ({ email, customId, signatureName }) => { - email.content.subject = `${customId}: Revision requested` - email.content.signatureName = signatureName - email.content.signatureJournal = journalName - - return email - }, - getHEComments: ({ heRecommendation }) => { - const heComments = get(heRecommendation, 'comments', []) - - if (heComments.length === 0) return - - const publicComment = heComments.find(comm => comm.public) - - const content = get(publicComment, 'content') - if (!content) { - throw new Error('a public comment cannot be without content') - } - const comments = `Please find our editorial comments below<br/>: "${content}"` - - return comments - }, - getAllAuthors: ({ title, comments, emailType, fragmentAuthors }) => { - const authors = fragmentAuthors.map(author => ({ - ...author, - ...getEmailCopy({ - comments, - emailType, - titleText: `your manuscript titled "${title}"`, - }), - })) - - return authors - }, - sendSubmittingAuthorEmail: ({ email, author, baseUrl }) => { - email.toUser = { - email: author.email, - name: `${author.lastName}`, - } - email.content.unsubscribeLink = services.createUrl( - baseUrl, - unsubscribeSlug, - { - id: author.id, - token: author.accessTokens.unsubscribe, - }, - ) - const { html, text } = email.getNotificationBody({ - emailBodyProps: { - paragraph: author.paragraph, - hasLink: author.hasLink, - hasIntro: author.hasIntro, - hasSignature: author.hasSignature, - }, - }) - email.sendEmail({ html, text }) - }, - updateEmailContentForAllAuthors: ({ email, recommendation, customId }) => { - switch (recommendation) { - case 'publish': - email.content.subject = `${customId}: Manuscript accepted` - break - case 'reject': - email.content.subject = `${customId}: Manuscript rejected` - break - default: - throw new Error(`Undefined recommendation: ${recommendation}`) - } - email.content.signatureJournal = journalName - - return email - }, - getEmailTypeByRecommendationForAuthors: ({ - recommendation, - hasPeerReview, - }) => { - let emailType - switch (recommendation) { - case 'publish': - emailType = 'author-manuscript-published' - break - case 'reject': - emailType = hasPeerReview - ? 'author-manuscript-rejected' - : 'authors-manuscript-rejected-before-review' - break - default: - throw new Error(`Undefined recommendation: ${recommendation}`) - } - - return emailType - }, -} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js deleted file mode 100644 index 9bb9c3a6cbd7d7b66222cb4b472e108c08304a06..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/notifications/notifications.js +++ /dev/null @@ -1,276 +0,0 @@ -const config = require('config') -const { get, isEmpty } = require('lodash') -const Email = require('@pubsweet/component-email-templating') - -const { - User, - services, - Fragment, - Collection, -} = require('pubsweet-component-helper-service') - -const { getEmailCopy } = require('./emailCopy') -const helpers = require('./helpers') - -const editorialAssistantEmail = config.get('journal.staffEmail') -const journalName = config.get('journal.name') -module.exports = { - async sendNotifications({ - hasEQA, - baseUrl, - fragment, - UserModel, - collection, - targetUserName, - isEditorInChief, - newRecommendation, - }) { - const fragmentHelper = new Fragment({ fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) - const { - activeAuthors, - submittingAuthor, - } = await fragmentHelper.getAuthorData({ UserModel }) - - const subjectBaseText = `${collection.customId}: Manuscript` - const titleText = `The manuscript titled "${parsedFragment.title}" by ${ - submittingAuthor.firstName - } ${submittingAuthor.lastName}` - - const userHelper = new User({ UserModel }) - const eicName = await userHelper.getEiCName() - - let email = new Email({ - type: 'user', - content: { - signatureName: eicName, - unsubscribeLink: baseUrl, - ctaText: 'MANUSCRIPT DETAILS', - signatureJournal: journalName, - ctaLink: services.createUrl( - baseUrl, - `/projects/${collection.id}/versions/${fragment.id}/details`, - ), - }, - }) - - const { recommendation, recommendationType } = newRecommendation - - // the EiC recommends to publish so an email to the EQA needs to be sent, - // one requesting approval or one informing them that the manuscript has been published - if ( - isEditorInChief && - recommendation === 'publish' && - collection.technicalChecks.token - ) { - sendEQAEmail({ - email, - eicName, - baseUrl, - titleText, - collection, - subjectBaseText, - }) - } - - const hasPeerReview = !isEmpty(collection.handlingEditor) - const { customId } = collection - const collHelper = new Collection({ collection }) - - // send HE emails when a review is submitted - // or when the EiC makes a recommendation after peer review - if ( - recommendationType === 'review' || - (isEditorInChief && - hasPeerReview && - (recommendation !== 'publish' || hasEQA)) - ) { - const handlingEditor = get(collection, 'handlingEditor', {}) - const heUser = await UserModel.find(handlingEditor.id) - email = helpers.updateEmailContentForHE({ - email, - baseUrl, - eicName, - customId, - recommendation, - recommendationType, - heLastName: collHelper.getHELastName(), - handlingEditor: heUser, - }) - const emailType = helpers.getEmailTypeByRecommendationForHE({ - recommendation, - recommendationType, - }) - const comments = helpers.getEiCCommentsForHE({ newRecommendation }) - helpers.sendHandlingEditorEmail({ - email, - comments, - emailType, - titleText, - targetUserName, - }) - } - - if ( - recommendationType === 'review' || - recommendation === 'return-to-handling-editor' - ) { - return - } - - // when publishing, only send emails to authors if the manuscript has passed EQA - if (isEditorInChief && (recommendation !== 'publish' || hasEQA)) { - // send all authors email - const emailType = helpers.getEmailTypeByRecommendationForAuthors({ - recommendation, - hasPeerReview, - }) - - let comments - if (hasPeerReview) { - comments = helpers.getHEComments({ - heRecommendation: parsedFragment.heRecommendation, - }) - } else { - comments = newRecommendation.comments[0].content - } - - const authors = helpers.getAllAuthors({ - comments, - emailType, - title: parsedFragment.title, - fragmentAuthors: activeAuthors, - }) - email = helpers.updateEmailContentForAllAuthors({ - email, - customId, - recommendation, - }) - helpers.sendAuthorsEmail({ email, authors, baseUrl }) - } - - // send email to SA when the HE requests a revision - if (collection.status === 'revisionRequested') { - const authorNoteText = helpers.getPrivateNoteTextForAuthor({ - newRecommendation, - }) - const author = helpers.getSubmittingAuthor({ - journalName, - authorNoteText, - submittingAuthor, - title: parsedFragment.title, - }) - email = helpers.updateEmailContentForSA({ - email, - customId, - signatureName: get(collection, 'handlingEditor.name', eicName), - }) - - helpers.sendSubmittingAuthorEmail({ email, author, baseUrl }) - } - - if (!hasPeerReview) { - return - } - - let reviewers = [] - if (isEditorInChief) { - if (recommendation !== 'publish' || hasEQA) { - email = helpers.updateEmailContentForReviewers({ - email, - customId, - recommendation, - signatureName: eicName, - }) - reviewers = await helpers.getSubmittedReviewers({ - UserModel, - titleText, - fragmentHelper, - recommendation, - }) - } - } else { - email = helpers.updateEmailContentForReviewers({ - email, - customId, - subject: `Review no longer required`, - signatureName: get(collection, 'handlingEditor.name', eicName), - }) - - reviewers = await helpers.getNoResponseReviewers({ - UserModel, - titleText, - fragmentHelper, - }) - - helpers.sendEiCsEmail({ - email, - baseUrl, - customId, - userHelper, - recommendation: newRecommendation, - targetUserName: collHelper.getHELastName(), - titleText: `the submission "${parsedFragment.title}" by ${ - submittingAuthor.firstName - } ${submittingAuthor.lastName}`, - }) - } - - helpers.sendReviewersEmail({ email, baseUrl, reviewers }) - }, -} - -const sendEQAEmail = ({ - email, - eicName, - baseUrl, - titleText, - collection, - subjectBaseText, -}) => { - let emailType - switch (collection.status) { - case 'accepted': - emailType = 'eqa-manuscript-published' - email.content.subject = `${subjectBaseText} decision finalized` - break - case 'inQA': - emailType = 'eqa-manuscript-request-for-approval' - 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' - break - default: - throw new Error( - `Cannot send EQA email when collection status is ${collection.status} `, - ) - } - - email.toUser = { - email: editorialAssistantEmail, - name: 'Editorial Assistant', - } - - email.content.unsubscribeLink = baseUrl - email.content.signatureName = eicName - - const { html, text } = email.getNotificationBody({ - emailBodyProps: getEmailCopy({ - eicName, - titleText, - emailType, - customId: collection.customId, - }), - }) - email.sendEmail({ html, text }) -} diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js index 43ea6d60498e6c90dceadec514060fe4d55bf3a9..7846722ed2f83db255a59ff80f95c142ccf2d3ec 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js @@ -4,7 +4,7 @@ const { Collection, } = require('pubsweet-component-helper-service') -const notifications = require('./notifications/notifications') +const Notification = require('../../notifications/notification') module.exports = models => async (req, res) => { const { collectionId, fragmentId, recommendationId } = req.params @@ -42,22 +42,22 @@ module.exports = models => async (req, res) => { }) const UserModel = models.User - const user = await UserModel.find(req.user) + const reviewer = await UserModel.find(req.user) Object.assign(recommendation, req.body) recommendation.updatedOn = Date.now() if (req.body.submittedOn) { - notifications.sendNotifications({ + const notification = new Notification({ fragment, + UserModel, collection, - isEditorInChief: false, - UserModel: models.User, baseUrl: services.getBaseUrl(req), newRecommendation: recommendation, - targetUserName: `${user.lastName}`, }) + notification.notifyHEWhenReviewerSubmitsReport(reviewer.lastName) + if (['underReview'].includes(collection.status)) { const collectionHelper = new Collection({ collection }) collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index b16803dbdb91e845d3a6e4b9b24fe30ca6776606..55d54aa4274c9035da2fe92342ccab04929b6220 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -1,5 +1,5 @@ const uuid = require('uuid') -const { pick, get, set, has } = require('lodash') +const { pick, get, set, has, isEmpty } = require('lodash') const config = require('config') const { v4 } = require('uuid') @@ -30,7 +30,7 @@ const sendMTSPackage = async (collection, fragment, isEQA = false) => { await MTS.sendPackage({ fragment: packageFragment, isEQA }) } -const notifications = require('./notifications/notifications') +const Notification = require('../../notifications/notification') module.exports = models => async (req, res) => { const { recommendation, comments, recommendationType } = req.body @@ -134,16 +134,50 @@ module.exports = models => async (req, res) => { await collection.save() } - notifications.sendNotifications({ - hasEQA, + const notification = new Notification({ fragment, collection, - isEditorInChief, newRecommendation, UserModel: models.User, - targetUserName: reqUser.lastName, baseUrl: services.getBaseUrl(req), }) + + const hasPeerReview = !isEmpty(collection.handlingEditor) + + if (isEditorInChief) { + if (recommendation === 'publish' && collection.status === 'inQA') { + notification.notifyEAWhenEiCRequestsEQAApproval() + } + + if (recommendation === 'publish' && collection.status === 'accepted') { + notification.notifyEAWhenEiCMakesFinalDecision() + } + + if (hasPeerReview && (recommendation !== 'publish' || hasEQA)) { + if (recommendation === 'return-to-handling-editor') { + notification.notifyHEWhenEiCReturnsToHE() + } else { + notification.notifyHEWhenEiCMakesDecision() + notification.notifyReviewersWhenEiCMakesDecision() + } + } + + if ( + recommendation !== 'return-to-handling-editor' && + (recommendation !== 'publish' || hasEQA) + ) { + notification.notifyAuthorsWhenEiCMakesDecision() + } + } else { + if (collection.status === 'revisionRequested') { + notification.notifySAWhenHERequestsRevision() + } + + if (hasPeerReview) { + notification.notifyReviewersWhenHEMakesRecommendation() + notification.notifyEiCWhenHEMakesRecommendation() + } + } } fragment.recommendations.push(newRecommendation) diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js deleted file mode 100644 index d454047c92f9d937a234ee0eb60baf89e0ed50b1..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/emailCopy.js +++ /dev/null @@ -1,46 +0,0 @@ -const config = require('config') - -// const staffEmail = config.get('journal.staffEmail') -const journalName = config.get('journal.name') - -const getEmailCopy = ({ emailType, titleText, comments }) => { - let paragraph - let hasLink = true - let hasIntro = true - let hasSignature = true - switch (emailType) { - case 'eqs-manuscript-accepted': - hasIntro = false - hasSignature = false - paragraph = `A new ${titleText} has been submitted to ${journalName}.<br/><br/> - To begin the review process, please visit the manuscript details page.` - 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, hasIntro, hasSignature } -} - -module.exports = { - getEmailCopy, -} diff --git a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js b/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js deleted file mode 100644 index 8380da030638ce5c7d66293fd7531ac01841a237..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/routes/technicalChecks/notifications/notifications.js +++ /dev/null @@ -1,105 +0,0 @@ -const config = require('config') -const Email = require('@pubsweet/component-email-templating') - -const { - User, - services, - Fragment, -} = require('pubsweet-component-helper-service') -const { getEmailCopy } = require('./emailCopy') - -const { name: journalName, staffEmail } = config.get('journal') - -module.exports = { - async sendNotifications({ - agree, - baseUrl, - collection, - comments = '', - User: UserModel, - Fragment: FragmentModel, - }) { - const fragment = await FragmentModel.find(collection.fragments[0]) - const fragmentHelper = new Fragment({ fragment }) - const parsedFragment = await fragmentHelper.getFragmentData({ - handlingEditor: collection.handlingEditor, - }) - - const { submittingAuthor } = await fragmentHelper.getAuthorData({ - UserModel, - }) - - const titleText = `manuscript titled "${parsedFragment.title}" by ${ - submittingAuthor.firstName - } ${submittingAuthor.lastName}` - const subjectBaseText = `${collection.customId}: Manuscript` - const { customId } = collection - const userHelper = new User({ UserModel }) - const subject = `${subjectBaseText} ${ - agree ? '' : 'Not ' - }Passed Technical Checks` - - const email = new Email({ - type: 'user', - fromEmail: `${journalName} <${staffEmail}>`, - content: { - subject, - signatureName: 'EQA Team', - ctaLink: services.createUrl( - baseUrl, - `/projects/${collection.id}/versions/${fragment.id}/details`, - ), - ctaText: 'MANUSCRIPT DETAILS', - unsubscribeLink: baseUrl, - }, - }) - - sendEditorsEmail({ - email, - agree, - customId, - comments, - titleText, - userHelper, - }) - }, -} - -const sendEditorsEmail = async ({ - email, - agree, - titleText, - customId, - userHelper, - comments = '', -}) => { - email.content.subject = `${customId}: New manuscript submitted` - 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.getNotificationBody({ - emailBodyProps: { - paragraph: eic.paragraph, - hasLink: eic.hasLink, - hasIntro: eic.hasIntro, - hasSignature: eic.hasSignature, - }, - }) - 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 fcb8a59daf70b1bc7129fa23b1fe664d8de5750e..c73ab4ad4f676df47d77fc7fd7ee281e06dd31a9 100644 --- a/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js +++ b/packages/component-manuscript-manager/src/routes/technicalChecks/patch.js @@ -1,13 +1,7 @@ const { get, find, isEmpty, last } = require('lodash') const { services } = require('pubsweet-component-helper-service') -const { - sendNotifications: sendEQSNotifications, -} = require('./notifications/notifications') - -const { - sendNotifications: sendEQANotifications, -} = require('../fragmentsRecommendations/notifications/notifications') +const Notification = require('../../notifications/notification') const TECHNICAL_STEPS = { EQS: 'eqs', @@ -57,7 +51,6 @@ module.exports = ({ Collection, Fragment, User }) => async (req, res) => { }) } - delete collection.technicalChecks.token if (agree) { if (step === TECHNICAL_STEPS.EQA) { collection.technicalChecks.eqa = true @@ -67,35 +60,37 @@ module.exports = ({ Collection, Fragment, User }) => async (req, res) => { } } + delete collection.technicalChecks.token collection.status = setNewStatus(step, agree) await collection.save() - const isEQA = get(collection, 'technicalChecks.eqa', false) - const baseUrl = services.getBaseUrl(req) - if (isEQA) { - const fragment = await Fragment.find(last(collection.fragments)) + const fragment = await Fragment.find(last(collection.fragments)) + const notification = new Notification({ + fragment, + collection, + UserModel: User, + baseUrl: services.getBaseUrl(req), + newRecommendation: { + recommendation: 'publish', + recommendationType: 'editorRecommendation', + }, + }) - sendEQANotifications({ - baseUrl, - fragment, - collection, - hasEQA: true, - UserModel: User, - isEditorInChief: true, - newRecommendation: { - recommendation: 'publish', - recommendationType: 'editorRecommendation', - }, - }) - } else { - sendEQSNotifications({ - User, - agree, - comments, - Fragment, - collection, - baseUrl, - }) + if (step === TECHNICAL_STEPS.EQA) { + const hasPassedEQA = get(collection, 'technicalChecks.eqa') + if (hasPassedEQA) { + // the EA accepted the manuscript so we need to notify users of the final publication decision + notification.notifyAuthorsWhenEiCMakesDecision() + const hasPeerReview = !isEmpty(collection.handlingEditor) + if (hasPeerReview) { + notification.notifyHEWhenEiCMakesDecision() + notification.notifyReviewersWhenEiCMakesDecision() + } + } else { + notification.notifyEiCWhenEQARejectsManuscript(comments) + } + } else if (agree) { + notification.notifyEiCWhenEQSAcceptsManuscript() } return res.status(200).json(collection) 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 e86097c9abb9defbad77862f1826360d5edef8fa..b090a9cb6fc1ae464488560f7032b0ff97388dea 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -306,8 +306,6 @@ describe('Post fragments recommendations route handler', () => { delete fragment.recommendations delete fragment.revision delete fragment.invitations - delete collection.invitations - delete collection.handlingEditor collection.technicalChecks.eqa = false const res = await requests.sendRequest({ diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index c3e8683307b2b599e99a166cef78f693f2b78ddb..ab97d296049c9240213c740f6987eb0cabd8f8ad 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -39,6 +39,8 @@ const ManuscriptLayout = ({ fragment = {}, changeForm, isFetching, + isFetchingData, + publonsFetching, fetchingError, formValues, heExpanded, @@ -84,7 +86,7 @@ const ManuscriptLayout = ({ fragment={fragment} handlingEditors={handlingEditors} inviteHE={toggleAssignHE} - isFetching={isFetching.editorsFetching} + isFetching={isFetchingData.editorsFetching} journal={journal} resendInvitation={inviteHandlingEditor.assignHE} revokeInvitation={inviteHandlingEditor.revokeHE} @@ -107,8 +109,12 @@ const ManuscriptLayout = ({ /> )} - {get(currentUser, 'permissions.canSubmitRevision', false) && ( - <SubmitRevision {...submitRevision} /> + {get(currentUser, 'permissions.authorCanViewReportsDetails', false) && ( + <AuthorReviews + currentUser={currentUser} + journal={journal} + reports={reviewerReports} + /> )} {submittedOwnRecommendation && ( @@ -161,7 +167,7 @@ const ManuscriptLayout = ({ currentUser={currentUser} expanded={heExpanded} handlingEditors={handlingEditors} - isFetching={isFetching.editorsFetching} + isFetching={isFetchingData.editorsFetching} toggle={toggleAssignHE} /> @@ -179,7 +185,7 @@ const ManuscriptLayout = ({ ) } invitations={invitationsWithReviewers} - isFetching={isFetching.publonsFetching} + isFetching={isFetchingData.publonsFetching} journal={journal} mb={2} publonReviewers={publonReviewers} @@ -189,12 +195,9 @@ const ManuscriptLayout = ({ {...inviteReviewer} /> )} - {get(currentUser, 'permissions.authorCanViewReportsDetails', false) && ( - <AuthorReviews - currentUser={currentUser} - journal={journal} - reports={reviewerReports} - /> + + {get(currentUser, 'permissions.canSubmitRevision', false) && ( + <SubmitRevision {...submitRevision} /> )} {get(currentUser, 'permissions.canMakeHERecommendation', false) && diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 0b77b64cdfeaecaa7e71b9089627d0aade3a2519..ab96f5c6b3bb159bf4e1d9f15ea20a341029e5a3 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -191,7 +191,7 @@ export default compose( ), }, }, - isFetching: { + isFetchingData: { editorsFetching: selectFetching(state), publonsFetching: isFetching, },