Skip to content
Snippets Groups Projects
Commit 36281a56 authored by Sebastian Mihalache's avatar Sebastian Mihalache
Browse files

feat: refactor/rebuild email templating service in component-invite and...

feat: refactor/rebuild email templating service in component-invite and component-manuscript-manager
parent 0e2e971d
No related branches found
No related tags found
2 merge requests!21Sprint #16 features,!17Email helper refactor
Showing
with 89 additions and 454 deletions
......@@ -21,11 +21,16 @@ class Fragment {
return owners
}
async getFragmentData({ handlingEditor = {} }) {
async getFragmentData({ handlingEditor } = {}) {
const { fragment: { metadata = {}, recommendations = [], id } } = this
const heRecommendation = recommendations.find(
rec => rec.userId === handlingEditor.id,
)
let heRecommendation
if (handlingEditor) {
heRecommendation = recommendations.find(
rec => rec.userId === handlingEditor.id,
)
}
let { title = '', abstract = '' } = metadata
const { type } = metadata
title = title.replace(/<(.|\n)*?>/g, '')
......@@ -73,10 +78,6 @@ class Fragment {
const userHelper = new User({ UserModel })
const activeAuthors = await userHelper.getActiveAuthors(authors)
// const authorsList = activeAuthors.map(
// author => `${author.firstName} ${author.lastName}`,
// )
return {
activeAuthors,
submittingAuthor,
......@@ -97,7 +98,7 @@ class Fragment {
)
: invitations.filter(inv => inv.role === role && inv.hasAnswer === false)
if (type === 'submitting') {
if (type === 'submitted') {
filteredInvitations = filteredInvitations.filter(inv =>
recommendations.find(
rec =>
......
......@@ -13,12 +13,9 @@ class Email {
},
content = {
subject: '',
comments: '',
titleText: '',
timestamp: '',
detailsLink: '',
ctaLink: '',
ctaText: '',
signatureName: '',
sourceUserName: '',
unsubscribeLink: '',
},
}) {
......@@ -39,164 +36,32 @@ class Email {
this.content = newContent
}
getBody({ emailType }) {
const replacements = {
hasLink: true,
toUserName: this.toUser.name,
beforeAnchor: 'Please visit the',
afterAnchor: 'to see the full review',
detailsLink: this.content.detailsLink,
signatureName: this.content.signatureName,
unsubscribeLink: this.content.unsubscribeLink,
}
switch (emailType) {
case 'unassign-reviewer':
replacements.paragraph = `You are no longer needed to review ${
this.content.titleText
}. If you have comments on this manuscript you believe the Editor should
see, please email them to ${config.get(
'mailer.from',
)} as soon as possible. Thank you for your
time and I hope you will consider reviewing for Hindawi again.`
break
case 'review-submitted':
replacements.paragraph = `We are pleased to inform you that Dr. ${
this.content.sourceUserName
} has submitted a review for ${this.content.titleText}.`
break
case 'reviewer-agreed':
// subject = `${meta.collection.customId}: Manuscript Reviews`
replacements.paragraph = `We are pleased to inform you that Dr. ${
this.content.sourceUserName
} has agreed to review ${this.content.titleText}. <br/>
You should receive the report by Dr. ${
this.content.sourceUserName
} before ${helpers.getExpectedDate(this.content.timestamp, 14)}.`
replacements.beforeAnchor =
'If you have any queries, or would like to send a reminder if you no report has been submitted, then please visit the'
break
case 'reviewer-declined':
// subject = `${meta.collection.customId}: Manuscript Reviews`
replacements.paragraph = `We regret to inform you that Dr. ${
this.content.sourceUserName
} has declined to review ${this.content.titleText}.`
replacements.afterAnchor =
'to see if you need to invite any additional reviewers in order to reach a decision on the manuscript'
break
case 'reviewer-thank-you':
// subject = `${meta.collection.customId}: Manuscript Review`
replacements.paragraph = `Thank you for agreeing to review ${
this.content.titleText
}.`
replacements.beforeAnchor =
'You can view the full PDF file of the manuscript and post your review report using the following URL:'
replacements.afterAnchor = ''
break
case 'agreed-reviewers-after-he-recommendation':
replacements.hasLink = false
replacements.paragraph = `I appreciate any time you may have spent reviewing ${
this.content.titleText
}. However, an editorial decision has been made and the review of this manuscript is now complete. I apologize for any inconvenience. <br/>
If you have comments on this manuscript you believe the Editor should see, please email them to Hindawi as soon as possible. <br/>
Thank you for your interest and I hope you will consider reviewing for Hindawi again.`
break
case 'pending-reviewers-after-he-recommendation':
replacements.hasLink = false
replacements.paragraph = `An editorial decision has been made regarding ${
this.content.titleText
}. So, you do not need to proceed with the review of this manuscript. <br/><br/>
If you have comments on this manuscript you believe the Editor should see, please email them to Hindawi as soon as possible.`
break
case 'eic-recommendation':
// subject = `${meta.collection.customId}: Manuscript Recommendation`
replacements.beforeAnchor =
'For more information about what is required, please visit the '
break
case 'author-request-to-revision':
replacements.paragraph = `In order for ${
this.content.titleText
} to proceed to publication, there needs to be a revision. <br/><br/>
${this.content.comments}<br/><br/>`
replacements.beforeAnchor =
'For more information about what is required, please visit the '
break
case 'author-manuscript-rejected':
replacements.paragraph = `I am sorry to inform you that ${
this.content.titleText
} has been rejected for publication. <br/><br/>
${this.content.comments}<br/><br/>`
replacements.hasLink = false
break
case 'author-manuscript-published':
replacements.paragraph = `I am delighted to inform you that ${
this.content.titleText
} has passed through the review process and will be published in Hindawi.<br/><br/>
${this.content.comments}<br/><br/>
Thanks again for choosing to publish with us.`
replacements.hasLink = false
break
case 'he-manuscript-rejected':
// subject = meta.emailSubject
replacements.hasLink = false
replacements.paragraph = `Thank you for your recommendation to reject ${
this.content.titleText
} based on the reviews you received.<br/><br/>
I can confirm this article has now been rejected.`
break
case 'he-manuscript-published':
replacements.hasLink = false
replacements.paragraph = `Thank you for your recommendation to publish ${
this.content.titleText
} based on the reviews you received.<br/><br/>
I can confirm this article will now go through to publication.`
break
case 'he-manuscript-return-with-comments':
// subject = meta.emailSubject
replacements.hasLink = false
replacements.paragraph = `Thank you for your recommendation for ${
this.content.titleText
} based on the reviews you received.<br/><br/>
${this.content.comments}<br/><br/>`
break
case 'submitting-reviewers-after-decision':
replacements.hasLink = false
replacements.paragraph = `Thank you for your review on ${
this.content.titleText
}. After taking into account the reviews and the recommendation of the Handling Editor, I can confirm this article ${
this.content.comments
}.<br/><br/>
If you have any queries about this decision, then please email them to Hindawi as soon as possible.`
break
case 'new-version-submitted':
replacements.paragraph = `A new version of ${
this.content.titleText
} has been submitted.`
replacements.beforeAnchor =
'Previous reviewers have been automatically invited to review the manuscript again. Please visit the'
replacements.afterAnchor =
'to see the latest version and any other actions you may need to take'
break
case 'submitting-reviewers-after-revision':
replacements.paragraph = `A new version of ${
this.content.titleText
} has been submitted.`
replacements.beforeAnchor = `As you have reviewed the previous version of this manuscript, I would be grateful if you can review this revised version and submit a review report by ${helpers.getExpectedDate(
this.content.timestamp,
14,
)}. You can download the PDF of the revised version and submit your new review from the following URL:`
break
default:
throw new Error(`undefined email type: ${emailType}`)
getBody({ body = {}, hasLink = true, isReviewerInvitation = false }) {
if (isReviewerInvitation) {
return {
html: helpers.getInvitationBody({
replacements: {
toUserName: this.toUser.name,
...this.content,
...body,
},
}),
text: `${this.content.signatureName}`,
}
}
return {
html: helpers.getNotificationBody('notification', replacements),
text: `${replacements.intro} ${replacements.paragraph} ${
replacements.beforeAnchor
} ${replacements.detailsUrl} ${replacements.afterAnchor} ${
replacements.signatureName
}`,
html: helpers.getNotificationBody({
replacements: {
hasLink,
paragraph: body.paragraph,
toUserName: this.toUser.name,
...this.content,
},
}),
text: `${body.paragraph} ${this.content.ctaLink} ${
this.content.ctaText
} ${this.content.signatureName}`,
}
}
......
const querystring = require('querystring')
// const querystring = require('querystring')
const fs = require('fs')
const handlebars = require('handlebars')
const createUrl = (baseUrl, slug, queryParams = null) =>
!queryParams
? `${baseUrl}${slug}`
: `${baseUrl}${slug}?${querystring.encode(queryParams)}`
const getEmailBody = (emailType, replacements) => {
const getNotificationBody = ({ replacements }) => {
handlePartial('header', replacements)
handlePartial('footer', replacements)
handlePartial('mainButton', replacements)
handlePartial('mainBody', replacements)
return getMainTemplate(emailType, replacements)
}
const getNotificationBody = (emailType, replacements) => {
handlePartial('notificationHeader', replacements)
handlePartial('footer', replacements)
handlePartial('signature', replacements)
if (replacements.hasLink) handlePartial('manuscriptDetailsLink', replacements)
handlePartial('notificationBody', replacements)
if (replacements.hasLink) handlePartial('button', replacements)
handlePartial('body', replacements)
return getMainTemplate(emailType, replacements)
return getMainTemplate({ fileName: 'notification', context: replacements })
}
const getInvitationBody = (emailType, replacements) => {
handlePartial('invitationHeader', replacements)
const getInvitationBody = ({ replacements }) => {
handlePartial('invHeader', replacements)
handlePartial('footer', replacements)
handlePartial('invitationUpperContent', replacements)
handlePartial('invitationButtons', replacements)
handlePartial('manuscriptData', replacements)
handlePartial('invUpperContent', replacements)
handlePartial('invButtons', replacements)
handlePartial('invManuscriptData', replacements)
handlePartial('signature', replacements)
handlePartial('invitationLowerContent', replacements)
handlePartial('invLowerContent', replacements)
return getMainTemplate(emailType, replacements)
}
const getBody = (emailType, replacements) => {
const simplePartials = ['header', 'footer', 'mainButton', 'mainBody']
const notificationPartials = [
'notificationHeader',
'footer',
'signature',
'manuscriptDetailsLink',
'notificationBody',
]
const invitationPartials = [
'invitationHeader',
'footer',
'invitationUpperContent',
'invitationButtons',
'manuscriptData',
'signature',
'invitationLowerContent',
]
switch (emailType) {
case 'simpleCTA':
case 'noCTA':
simplePartials.forEach(partial => handlePartial(partial, replacements))
break
case 'invitation':
invitationPartials.forEach(partial =>
handlePartial(partial, replacements),
)
break
case 'notification':
notificationPartials.forEach(partial =>
handlePartial(partial, replacements),
)
break
default:
break
}
return getMainTemplate(emailType, replacements)
return getMainTemplate({ fileName: 'invitation', context: replacements })
}
const readFile = path =>
......@@ -94,30 +40,14 @@ const handlePartial = (partialName = 'signature', context = {}) => {
handlebars.registerPartial(partialName, partial)
}
const getMainTemplate = (fileName, context) => {
const getMainTemplate = ({ fileName, context }) => {
const htmlFile = readFile(`${__dirname}/templates/${fileName}.html`)
const htmlTemplate = handlebars.compile(htmlFile)
const htmlBody = htmlTemplate(context)
return htmlBody
}
const getExpectedDate = (timestamp, daysExpected) => {
const date = new Date(timestamp)
let expectedDate = date.getDate() + daysExpected
date.setDate(expectedDate)
expectedDate = date.toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
return expectedDate
}
module.exports = {
getBody,
createUrl,
getEmailBody,
getExpectedDate,
getNotificationBody,
getInvitationBody,
}
{{> invitationHeader }}
{{> invitationUpperContent }}
{{> invitationButtons }}
{{> invitationLowerContent }}
{{> invHeader }}
{{> invUpperContent }}
{{> invButtons }}
{{> invLowerContent }}
{{> footer }}
\ No newline at end of file
{{> header }}
{{> mainBody }}
{{> footer }}
{{> notificationHeader}}
{{> notificationBody}}
{{> header }}
{{> body }}
{{#if hasLink }}
{{> button }}
{{/if}}
{{> footer}}
\ No newline at end of file
......@@ -6,9 +6,6 @@
<p>&nbsp;</p>
<p>
{{{paragraph}}}
{{#if hasLink }}
{{> manuscriptDetailsLink}}
{{/if}}
</p>
<p>&nbsp;</p>
{{> signature}}
......
......@@ -145,7 +145,7 @@
width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tr>
<td role="module-content">
<p>{{ previewText }}</p>
<p>you have a new notification</p>
</td>
</tr>
</table>
......
......@@ -27,7 +27,7 @@
<tr>
<td align="center" bgcolor="#0d78f2" class="inner-td" style="border-radius:6px;font-size:16px;text-align:center;background-color:inherit">
<a style="background-color:#0d78f2;border:1px solid #333333;border-color:#0d78f2;border-radius:0px;border-width:1px;color:#ffffff;display:inline-block;font-family:arial,helvetica,sans-serif;font-size:16px;font-weight:normal;letter-spacing:0px;line-height:16px;padding:12px 18px 12px 18px;text-align:center;text-decoration:none"
href="{{agreeUrl}}" target="_blank">AGREE</a>
href="{{ agreeLink }}" target="_blank">AGREE</a>
</td>
</tr>
</tbody>
......@@ -62,7 +62,7 @@
<tr>
<td align="center" bgcolor="#e4dfdf" class="inner-td" style="border-radius:6px;font-size:16px;text-align:center;background-color:inherit">
<a style="background-color:#e4dfdf;border:1px solid #333333;border-color:#E4DFDF;border-radius:0px;border-width:1px;color:#302e2e;display:inline-block;font-family:arial,helvetica,sans-serif;font-size:16px;font-weight:normal;letter-spacing:0px;line-height:16px;padding:12px 18px 12px 18px;text-align:center;text-decoration:none"
href="{{declineUrl}}" target="_blank">DECLINE</a>
href="{{ declineLink }}" target="_blank">DECLINE</a>
</td>
</tr>
</tbody>
......
......@@ -145,7 +145,7 @@
width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tr>
<td role="module-content">
<p>{{ previewText }}</p>
<p>new invitation</p>
</td>
</tr>
</table>
......
......@@ -2,12 +2,12 @@
<tr>
<td style="padding:30px 23px 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff">
<p data-pm-slice="1 1 []">
<a href="{{ manuscriptDetailsUrl }}">See more information</a>
<a href="{{ detailsLink }}">See more information</a>
</p>
<p data-pm-slice="1 1 []">&nbsp;</p>
{{> manuscriptData }}
{{> invManuscriptData }}
<p data-pm-slice="1 1 []">{{{ lowerContent }}}</p>
<p>&nbsp;</p>
......
......@@ -2,7 +2,7 @@
<tr>
<td style="padding:30px 23px 20px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff">
<div>
<p data-pm-slice="1 1 []">{{ intro }},</p>
<p data-pm-slice="1 1 []">Dear Dr. {{ toUserName }},</p>
<p>&nbsp;</p>
<p>{{ upperContent }}</p>
......
<table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;">
<tr>
<td style="padding:30px 23px 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff">
<h1 style="text-align: center;">{{ headline }}</h1>
<div style="text-align: center;">{{ paragraph }}</div>
<div style="text-align: center;">&nbsp;</div>
<div style="text-align: center;">&nbsp;</div>
</td>
</tr>
</table>
\ No newline at end of file
{{ beforeAnchor }} <a href="{{ detailsLink }}">manuscript details page</a> {{ afterAnchor}}.
\ No newline at end of file
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {width: 600px;margin: 0 auto;}
table {border-collapse: collapse;}
table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;}
img {-ms-interpolation-mode: bicubic;}
</style>
<![endif]-->
<style type="text/css">
body,
p,
div {
font-family: helvetica, arial, sans-serif;
font-size: 14px;
}
body {
color: #626262;
}
body a {
color: #0D78F2;
text-decoration: none;
}
p {
margin: 0;
padding: 0;
}
table.wrapper {
width: 100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
@media screen and (max-width:480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 480px !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
</style>
<!--user entered Head Start-->
<!--End Head user entered-->
</head>
<body>
<center class="wrapper" data-link-color="#0D78F2" data-body-style="font-size: 14px; font-family: helvetica,arial,sans-serif; color: #626262; background-color: #F4F4F4;">
<div class="webkit">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#F4F4F4">
<tr>
<td valign="top" bgcolor="#F4F4F4" width="100%">
<table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0">
<tr>
<td width="100%">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%; max-width:600px;" align="center">
<tr>
<td role="modules-container" style="padding: 0px 0px 0px 0px; color: #626262; text-align: left;" bgcolor="#F4F4F4" width="100%"
align="left">
<table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0"
width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tr>
<td role="module-content">
<p>you have a new notification</p>
</td>
</tr>
</table>
<table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;">
<tr>
<td style="font-size:6px;line-height:10px;padding:20px 0px 20px 0px;" valign="top" align="center">
<img class="max-width" border="0" style="display:block;color:#000000;text-decoration:none;font-family:Helvetica, arial, sans-serif;font-size:16px;max-width:10% !important;width:10%;height:auto !important;"
src="https://marketing-image-production.s3.amazonaws.com/uploads/bb39b20cf15e52c1c0933676e25f2b2402737c6560b8098c204ad6932b84eb2058804376dbc4db138c7a21dcaed9325bde36185648afac5bc97e3d73d4e12718.png"
alt="" width="60">
</td>
</tr>
</table>
\ No newline at end of file
{{> header }}
{{> mainBody }}
{{> mainButton }}
{{> footer }}
......@@ -81,10 +81,24 @@ const createUrl = (baseUrl, slug, queryParams = null) =>
? `${baseUrl}${slug}`
: `${baseUrl}${slug}?${querystring.encode(queryParams)}`
const getExpectedDate = ({ timestamp = Date.now(), daysExpected = 0 }) => {
const date = new Date(timestamp)
let expectedDate = date.getDate() + daysExpected
date.setDate(expectedDate)
expectedDate = date.toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
return expectedDate
}
module.exports = {
checkForUndefinedParams,
validateEmailAndToken,
handleNotFoundError,
getBaseUrl,
createUrl,
getExpectedDate,
}
// const mailService = require('pubsweet-component-mail-service')
const {
services,
Team,
services,
authsome: authsomeHelper,
} = require('pubsweet-component-helper-service')
const notifications = require('./emails/notifications')
module.exports = models => async (req, res) => {
const { collectionId, invitationId } = req.params
const teamHelper = new Team({ TeamModel: models.Team, collectionId })
......@@ -56,10 +56,14 @@ module.exports = models => async (req, res) => {
user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
await user.save()
// mailService.sendSimpleEmail({
// toEmail: user.email,
// emailType: 'revoke-handling-editor',
// })
notifications.sendNotifications({
models,
collection,
isEiC: true,
invitedHE: user,
isCanceled: true,
baseUrl: services.getBaseUrl(req),
})
return res.status(200).json({})
} catch (e) {
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment