From 317acbab8ce806bc8580d624e2de7253830ef8c6 Mon Sep 17 00:00:00 2001 From: Sebastian <sebastian.mihalache@thinslices.com> Date: Tue, 24 Apr 2018 19:48:28 +0300 Subject: [PATCH] feat(component-invite): resend reviewer invite --- .../src/helpers/Collection.js | 167 +++++---- .../src/helpers/Invitation.js | 69 ++-- .../src/routes/collectionsInvitations/post.js | 102 +++--- .../tests/collectionsInvitations/post.test.js | 6 +- packages/component-mail-service/src/Mail.js | 61 ++-- .../src/templates/invite-reviewer.html | 2 +- .../src/templates/invite-reviewer.txt | 2 +- .../src/templates/resend-reviewer.html | 335 ++++++++++++++++++ .../src/templates/resend-reviewer.txt | 20 ++ 9 files changed, 579 insertions(+), 185 deletions(-) create mode 100644 packages/component-mail-service/src/templates/resend-reviewer.html create mode 100644 packages/component-mail-service/src/templates/resend-reviewer.txt diff --git a/packages/component-invite/src/helpers/Collection.js b/packages/component-invite/src/helpers/Collection.js index a673d629b..c1a4c3a33 100644 --- a/packages/component-invite/src/helpers/Collection.js +++ b/packages/component-invite/src/helpers/Collection.js @@ -5,85 +5,98 @@ const config = require('config') const last = require('lodash/last') const statuses = config.get('statuses') -module.exports = { - addInvitation: async (collection, userId, role) => { - collection.invitations = collection.invitations || [] - let matchingInvitation = collection.invitations.find( - invitation => - invitation.userId === userId && - invitation.role === role && - invitation.hasAnswer === false, + +const addInvitation = async (collection, userId, role) => { + collection.invitations = collection.invitations || [] + let matchingInvitation = collection.invitations.find( + invitation => + invitation.userId === userId && + invitation.role === role && + invitation.hasAnswer === false, + ) + if (matchingInvitation === undefined) { + matchingInvitation = await invitationHelper.setupInvitation( + userId, + role, + collection, ) - if (matchingInvitation === undefined) { - matchingInvitation = await invitationHelper.setupInvitation( - userId, - role, - collection, - ) - } + } else { matchingInvitation.timestamp = Date.now() - await collection.save() - return matchingInvitation - }, - addAuthor: async (collection, user, res, url) => { - if (collection.owners.includes(user.id)) { - return res.status(200).json(user) - } - try { - await mailService.setupAssignEmail(user.email, 'assign-author', url) + } - return res.status(200).json(user) - } catch (e) { - logger.error(e) - return res.status(500).json({ error: 'Email could not be sent.' }) - } - }, - addHandlingEditor: async (collection, user, invitation) => { - collection.handlingEditor = { - id: user.id, - name: `${user.firstName} ${user.lastName}`, - timestamp: invitation.timestamp, - email: user.email, - hasAnswer: invitation.hasAnswer, - isAccepted: invitation.isAccepted, - } - collection.status = 'heInvited' - collection.visibleStatus = statuses[collection.status].private - await collection.save() - }, - updateHandlingEditor: async (collection, isAccepted) => { - collection.handlingEditor.hasAnswer = true - collection.handlingEditor.isAccepted = isAccepted - collection.handlingEditor.timestamp = Date.now() - if (isAccepted) { - collection.status = 'heAssigned' - } else { - collection.status = 'submitted' - } - collection.visibleStatus = statuses[collection.status].private - await collection.save() - }, - getFragmentAndAuthorData: async (models, collection) => { - const fragment = await models.Fragment.find(last(collection.fragments)) - let { title } = fragment.metadata - title = title.replace(/<(.|\n)*?>/g, '') + await collection.save() + return matchingInvitation +} - const submittingAuthorData = collection.authors.find( - author => author.isSubmitting === true, - ) - const author = await models.User.find(submittingAuthorData.userId) - const authorName = `${author.firstName} ${author.lastName}` - const { id } = fragment - return { title, authorName, id } - }, - updateReviewerCollectionStatus: async collection => { - const reviewerInvitations = collection.invitations.filter( - inv => inv.role === 'reviewer', - ) - if (reviewerInvitations.length === 0) { - collection.status = 'heAssigned' - collection.visibleStatus = statuses[collection.status].private - } - await collection.save() - }, +const addAuthor = async (collection, user, res, url) => { + if (collection.owners.includes(user.id)) { + return res.status(200).json(user) + } + try { + await mailService.setupAssignEmail(user.email, 'assign-author', url) + + return res.status(200).json(user) + } catch (e) { + logger.error(e) + return res.status(500).json({ error: 'Email could not be sent.' }) + } +} + +const addHandlingEditor = async (collection, user, invitation) => { + collection.handlingEditor = { + id: user.id, + name: `${user.firstName} ${user.lastName}`, + timestamp: invitation.timestamp, + email: user.email, + hasAnswer: invitation.hasAnswer, + isAccepted: invitation.isAccepted, + } + await updateStatus(collection, 'heInvited') +} + +const updateHandlingEditor = async (collection, isAccepted) => { + collection.handlingEditor.hasAnswer = true + collection.handlingEditor.isAccepted = isAccepted + collection.handlingEditor.timestamp = Date.now() + let status + isAccepted ? (status = 'heAssigned') : (status = 'submitted') + await updateStatus(collection, status) +} + +const getFragmentAndAuthorData = async (models, collection) => { + const fragment = await models.Fragment.find(last(collection.fragments)) + let { title } = fragment.metadata + title = title.replace(/<(.|\n)*?>/g, '') + + const submittingAuthorData = collection.authors.find( + author => author.isSubmitting === true, + ) + const author = await models.User.find(submittingAuthorData.userId) + const authorName = `${author.firstName} ${author.lastName}` + const { id } = fragment + return { title, authorName, id } +} + +const updateReviewerCollectionStatus = async collection => { + const reviewerInvitations = collection.invitations.filter( + inv => inv.role === 'reviewer', + ) + if (reviewerInvitations.length === 0) return + await updateStatus(collection, 'heAssigned') +} + +const updateStatus = async (collection, newStatus) => { + collection.status = newStatus + collection.visibleStatus = statuses[collection.status].private + await collection.save() +} + +module.exports = { + addInvitation, + addAuthor, + addHandlingEditor, + updateHandlingEditor, + getFragmentAndAuthorData, + updateReviewerCollectionStatus, + updateStatus, } diff --git a/packages/component-invite/src/helpers/Invitation.js b/packages/component-invite/src/helpers/Invitation.js index 855b265cf..29a252e71 100644 --- a/packages/component-invite/src/helpers/Invitation.js +++ b/packages/component-invite/src/helpers/Invitation.js @@ -30,53 +30,66 @@ const setupInvitation = async (userId, role, collection) => { return invitation } -const setupReviewerInvitation = async ( - req, - models, +const setupReviewerInvitation = async ({ + baseUrl, + FragmentModel, + UserModel, collection, user, mailService, invitationId, - invitationTimestamp, -) => { - const baseUrl = `${req.protocol}://${req.get('host')}` - const fragment = await models.Fragment.find(collection.fragments[0]) + timestamp, + resend = false, +}) => { + const fragment = await FragmentModel.find(collection.fragments[0]) const submittingAuthorData = collection.authors.find( author => author.isSubmitting === true, ) - const submittingAuthor = await models.User.find(submittingAuthorData.userId) + const submittingAuthor = await UserModel.find(submittingAuthorData.userId) const authorsPromises = collection.authors.map(async author => { - const user = await models.User.find(author.userId) + const user = await UserModel.find(author.userId) return `${user.firstName} ${user.lastName}` }) const authors = await Promise.all(authorsPromises) let { abstract, title } = fragment.metadata title = title.replace(/<(.|\n)*?>/g, '') abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : '' - const date = new Date(invitationTimestamp) - let expectedDate = date.getDate() + 14 - date.setDate(expectedDate) - expectedDate = date.toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - }) - await mailService.setupReviewerInvitationEmail( - user, + + const params = { + invitedUser: user, baseUrl, - collection, - abstract, - title, - submittingAuthor, - authors, - fragment.id, - invitationId, - expectedDate, - ) + collection: { + id: collection.id, + authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, + handlingEditor: collection.handlingEditor, + }, + subject: `${collection.customId}: Review Requested`, + fragment: { + id: fragment.id, + title, + abstract, + authors, + }, + invitation: { + id: invitationId, + timestamp, + }, + resend, + } + await mailService.setupReviewerInvitationEmail(params) } +const getInvitation = (invitations = [], userId, role) => + invitations.find( + invitation => + invitation.userId === userId && + invitation.role === role && + invitation.hasAnswer === false, + ) + module.exports = { getInvitationData, setupInvitation, setupReviewerInvitation, + getInvitation, } diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js index abffa83d3..09987c365 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/post.js +++ b/packages/component-invite/src/routes/collectionsInvitations/post.js @@ -9,7 +9,7 @@ const userHelper = require('../../helpers/User') const invitationHelper = require('../../helpers/Invitation') const configRoles = config.get('roles') -const statuses = config.get('statuses') + module.exports = models => async (req, res) => { const { email, role } = req.body @@ -40,51 +40,64 @@ module.exports = models => async (req, res) => { error: notFoundError.message, }) } - const url = `${req.protocol}://${req.get('host')}` + const baseUrl = `${req.protocol}://${req.get('host')}` + const params = { + baseUrl, + FragmentModel: models.Fragment, + UserModel: models.User, + collection, + mailService, + resend: false, + } try { const user = await models.User.findByEmail(email) await teamHelper.setupManuscriptTeam(models, user, collectionId, role) - - // find if there already is a matching invitation in the collection - const invitation = await collectionHelper.addInvitation( - collection, + let invitation = invitationHelper.getInvitation( + collection.invitations, user.id, role, ) - if (role === 'handlingEditor') { - await collectionHelper.addHandlingEditor(collection, user, invitation) - } - if (role === 'reviewer' && collection.status === 'heAssigned') { - collection.status = 'reviewersInvited' - collection.visibleStatus = statuses[collection.status].private - collection = await collection.save() + + let resend = false + if (invitation !== undefined) { + invitation.timestamp = Date.now() + await collection.save() + resend = true + } else { + invitation = await invitationHelper.setupInvitation( + user.id, + role, + collection, + ) } + try { + if (role === 'reviewer') { + if (collection.status === 'heAssigned') + await collectionHelper.updateStatus(collection, 'reviewersInvited') + + await invitationHelper.setupReviewerInvitation({ + ...params, + user, + invitationId: invitation.id, + timestamp: invitation.timestamp, + resend, + }) + } + if (role === 'handlingEditor') { + invitation.timestamp = Date.now() + await collection.save() + await collectionHelper.addHandlingEditor(collection, user, invitation) await mailService.setupAssignEmail( user.email, 'assign-handling-editor', - url, - ) - } else if (role === 'reviewer') { - await invitationHelper.setupReviewerInvitation( - req, - models, - collection, - user, - mailService, - invitation.id, - invitation.timestamp, + baseUrl, ) - } else { - return res.status(500).json({ - error: 'Something went wrong', - }) } - - return res.status(200).json(user) + return res.status(200).json(invitation) } catch (e) { logger.error(e) return res.status(500).json({ error: 'Email could not be sent.' }) @@ -93,7 +106,7 @@ module.exports = models => async (req, res) => { if (role === 'reviewer') { const newUser = await userHelper.setupNewUser( req.body, - url, + baseUrl, res, email, role, @@ -105,27 +118,22 @@ module.exports = models => async (req, res) => { error: newUser.message, }) } - if (collection.status === 'heAssigned') { - collection.status = 'reviewersInvited' - collection.visibleStatus = statuses[collection.status].private - collection = await collection.save() - } + if (collection.status === 'heAssigned') + await collectionHelper.updateStatus(collection, 'reviewersInvited') await teamHelper.setupManuscriptTeam(models, newUser, collectionId, role) - const invitation = await collectionHelper.addInvitation( - collection, + const invitation = await invitationHelper.setupInvitation( newUser.id, role, - ) - await invitationHelper.setupReviewerInvitation( - req, - models, collection, - newUser, - mailService, - invitation.id, - invitation.timestamp, ) - return res.status(200).json(newUser) + + await invitationHelper.setupReviewerInvitation({ + ...params, + newUser, + invitationId: invitation.id, + timestamp: invitation.timestamp, + }) + return res.status(200).json(invitation) } const notFoundError = await helpers.handleNotFoundError(e, 'user') return res.status(notFoundError.status).json({ diff --git a/packages/component-invite/src/tests/collectionsInvitations/post.test.js b/packages/component-invite/src/tests/collectionsInvitations/post.test.js index 5378ad0e6..d72bd114d 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/post.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/post.test.js @@ -69,7 +69,7 @@ describe('Post collections invitations route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.email).toEqual(body.email) + expect(data.role).toEqual(body.role) }) it('should return success when the a reviewer is invited', async () => { const { user, editorInChief } = testFixtures.users @@ -88,7 +88,7 @@ describe('Post collections invitations route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.email).toEqual(body.email) + expect(data.role).toEqual(body.role) }) it('should return an error when inviting his self', async () => { const { editorInChief } = testFixtures.users @@ -134,6 +134,6 @@ describe('Post collections invitations route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.email).toEqual(body.email) + expect(data.role).toEqual(body.role) }) }) diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index 17bcee132..dc5282a44 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -137,27 +137,23 @@ module.exports = { return Email.send(mailData) }, - setupReviewerInvitationEmail: async ( + setupReviewerInvitationEmail: async ({ invitedUser, baseUrl, - collection, - abstract, - title, - submittingAuthor, - authors, - fragmentId, - invitationId, - expectedDate, - ) => { - const subject = `${collection.customId}: Review Requested` - let agreeUrl = `${baseUrl}/projects/${ - collection.id - }/versions/${fragmentId}/details?${querystring.encode({ - invitationId, - agree: true, - })}` + collection: { id: collectionId, authorName, handlingEditor }, + fragment: { title, id: fragmentId, abstract, authors }, + invitation: { id: invitationId, timestamp }, + resend, + subject, + }) => { + let agreeUrl = `${baseUrl}/projects/${collectionId}/versions/${fragmentId}/details?${querystring.encode( + { + invitationId, + agree: true, + }, + )}` const declineUrl = `${baseUrl}${resetPasswordPath}?${querystring.encode({ - collectionId: collection.id, + collectionId, agree: false, invitationId, invitationToken: invitedUser.invitationToken, @@ -166,30 +162,39 @@ module.exports = { agreeUrl = `${baseUrl}${resetPasswordPath}?${querystring.encode({ email: invitedUser.email, token: invitedUser.passwordResetToken, - collectionId: collection.id, + collectionId, fragmentId, agree: true, invitationId, })}` } + const daysExpected = resend ? 0 : 14 + + const date = new Date(timestamp) + let expectedDate = date.getDate() + daysExpected + date.setDate(expectedDate) + expectedDate = date.toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) + const replacements = { - name: `${invitedUser.firstName} ${invitedUser.lastName}`, + reviewerName: `${invitedUser.firstName} ${invitedUser.lastName}`, agreeUrl, declineUrl, baseUrl, title, + editorName: handlingEditor.name, + submittingAuthorName: authorName, + expectedDate, abstract, - editorName: collection.handlingEditor.name, - editorEmail: collection.handlingEditor.email, - submittingAuthorName: `${submittingAuthor.firstName} ${ - submittingAuthor.lastName - }`, + editorEmail: handlingEditor.email, authors, - expectedDate, } - - const { htmlBody, textBody } = getEmailBody('invite-reviewer', replacements) + const emailTemplate = resend ? 'resend-reviewer' : 'invite-reviewer' + const { htmlBody, textBody } = getEmailBody(emailTemplate, replacements) const mailData = { from: config.get('mailer.from'), to: invitedUser.email, diff --git a/packages/component-mail-service/src/templates/invite-reviewer.html b/packages/component-mail-service/src/templates/invite-reviewer.html index d286cb63c..cfe6ed5b4 100644 --- a/packages/component-mail-service/src/templates/invite-reviewer.html +++ b/packages/component-mail-service/src/templates/invite-reviewer.html @@ -164,7 +164,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 []">Dear {{name}},</p> + <p data-pm-slice="1 1 []">Dear {{reviewerName}},</p> <p> </p> diff --git a/packages/component-mail-service/src/templates/invite-reviewer.txt b/packages/component-mail-service/src/templates/invite-reviewer.txt index b12453439..946571853 100644 --- a/packages/component-mail-service/src/templates/invite-reviewer.txt +++ b/packages/component-mail-service/src/templates/invite-reviewer.txt @@ -1,5 +1,5 @@ -Dear Dr. {{name}}, +Dear Dr. {{reviewerName}}, A manuscript titled {{title}}" by {{submittingAuthorName}}, has been submitted for possible publication in Hindawi. As the Academic Editor handling the manuscript, diff --git a/packages/component-mail-service/src/templates/resend-reviewer.html b/packages/component-mail-service/src/templates/resend-reviewer.html new file mode 100644 index 000000000..8840a4579 --- /dev/null +++ b/packages/component-mail-service/src/templates/resend-reviewer.html @@ -0,0 +1,335 @@ +<!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>review request</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> + + <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 20px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff"> + <div> + <p data-pm-slice="1 1 []">Dear {{reviewerName}},</p> + + <p> </p> + <p>On {{expectedDate}}, I sent you a request to review the manuscript titled "{{title}}" + by {{submittingAuthorName}}, submitted for possible publication in Hindawi. + </p> + <p>I would be grateful if you would agree to review it and let me know whether you feel + it is suitable for publication. If you are unable to review this manuscript then + please decline to review. More details are available by clicking the link.</p> + </div> + + </td> + </tr> + </table> + + <table border="0" cellpadding="0" cellspacing="0" align="center" width="100%" role="module" data-type="columns" data-version="2" + style="padding:20px 0px 20px 0px;background-color:#ffffff;box-sizing:border-box;" bgcolor="#ffffff"> + <tr role='module-content'> + <td height="100%" valign="top"> + <!--[if (gte mso 9)|(IE)]> + <center> + <table cellpadding="0" cellspacing="0" border="0" width="100%" style="border-spacing:0;border-collapse:collapse;table-layout: fixed;" > + <tr> + <![endif]--> + + <!--[if (gte mso 9)|(IE)]> + <td width="300.000px" valign="top" style="padding: 0px 0px 0px 0px;border-collapse: collapse;" > + <![endif]--> + + <table width="300.000" style="width:300.000px;border-spacing:0;border-collapse:collapse;margin:0px 0px 0px 0px;" cellpadding="0" + cellspacing="0" align="left" border="0" bgcolor="#ffffff" class="column column-0 of-2 + empty"> + <tr> + <td style="padding:0px;margin:0px;border-spacing:0;"> + <table border="0" cellPadding="0" cellSpacing="0" class="module" data-role="module-button" data-type="button" role="module" + style="table-layout:fixed" width="100%"> + <tbody> + <tr> + <td align="center" class="outer-td" style="padding:0px 0px 0px 0px"> + <table border="0" cellPadding="0" cellSpacing="0" class="button-css__deep-table___2OZyb wrapper-mobile" style="text-align:center"> + <tbody> + <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> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </table> + + <!--[if (gte mso 9)|(IE)]> + </td> + <![endif]--> + <!--[if (gte mso 9)|(IE)]> + <td width="300.000px" valign="top" style="padding: 0px 0px 0px 0px;border-collapse: collapse;" > + <![endif]--> + + <table width="300.000" style="width:300.000px;border-spacing:0;border-collapse:collapse;margin:0px 0px 0px 0px;" cellpadding="0" + cellspacing="0" align="left" border="0" bgcolor="#ffffff" class="column column-1 of-2 + empty"> + <tr> + <td style="padding:0px;margin:0px;border-spacing:0;"> + <table border="0" cellPadding="0" cellSpacing="0" class="module" data-role="module-button" data-type="button" role="module" + style="table-layout:fixed" width="100%"> + <tbody> + <tr> + <td align="center" class="outer-td" style="padding:0px 0px 0px 0px"> + <table border="0" cellPadding="0" cellSpacing="0" class="button-css__deep-table___2OZyb wrapper-mobile" style="text-align:center"> + <tbody> + <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> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </table> + + <!--[if (gte mso 9)|(IE)]> + </td> + <![endif]--> + <!--[if (gte mso 9)|(IE)]> + <tr> + </table> + </center> + <![endif]--> + </td> + </tr> + </table> + + <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"> + <p data-pm-slice="1 1 []"> + <a href="{{baseUrl}}">See more information</a> + </p> + + <p data-pm-slice="1 1 []"> </p> + + <p data-pm-slice="1 1 []">I would like to thank you in advance for your help with the evaluation of this manuscript, + since it would not be possible for us to run the journal without the help of our reviewers.</p> + + <p> </p> + + <p>I am looking forward to hearing from you.</p> + + <p> </p> + + + + <p>Best regards, + <br /> {{editorName}} + </p> + + <div style="text-align: center;"> </div> + + </td> + </tr> + </table> + <div data-role="module-unsubscribe" class="module unsubscribe-css__unsubscribe___2CDlR" role="module" data-type="unsubscribe" + style="color:#444444;font-size:12px;line-height:20px;padding:16px 16px 16px 16px;text-align:center"> + <div class="Unsubscribe--addressLine"> + <p class="Unsubscribe--senderName" style="font-family:Arial,Helvetica, sans-serif;font-size:12px;line-height:20px">Hindawi Publishing Corporation</p> + <p style="font-family:Arial,Helvetica, sans-serif;font-size:12px;line-height:20px"> + <span class="Unsubscribe--senderAddress">315 Madison Ave, Third Floor, Suite 3070</span>, + <span class="Unsubscribe--senderCity">NEW YORK</span>, + <span class="Unsubscribe--senderState">NY</span> + <span class="Unsubscribe--senderZip">10017</span> + </p> + </div> + <p style="font-family:Arial,Helvetica, sans-serif;font-size:12px;line-height:20px"> + <a class="Unsubscribe--unsubscribeLink" href="[Unsubscribe]">Unsubscribe</a> + </p> + </div> + </td> + </tr> + </table> + <!--[if mso]> + </td></tr></table> + </center> + <![endif]--> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </div> + </center> +</body> + +</html> \ No newline at end of file diff --git a/packages/component-mail-service/src/templates/resend-reviewer.txt b/packages/component-mail-service/src/templates/resend-reviewer.txt new file mode 100644 index 000000000..b9d171afb --- /dev/null +++ b/packages/component-mail-service/src/templates/resend-reviewer.txt @@ -0,0 +1,20 @@ +Dear {{reviewerName}}, + +On {{expectedDate}}, I sent you a request to review the manuscript titled "{{title}}" +by {{submittingAuthorName}}, submitted for possible publication in Hindawi. + +I would be grateful if you would agree to review it and let me know whether you feel +it is suitable for publication. If you are unable to review this manuscript then +please decline to review. More details are available by clicking the link. + +{{agreeUrl}} AGREE +{{declineUrl}} DECLINE +{{baseUrl}} See more information + +I would like to thank you in advance for your help with the evaluation of this manuscript, +since it would not be possible for us to run the journal without the help of our reviewers. + +I am looking forward to hearing from you. + +Best regards, +{{editorName}} \ No newline at end of file -- GitLab