diff --git a/packages/component-invite/src/controllers/assignCollectionRole.js b/packages/component-invite/src/controllers/assignCollectionRole.js index cd855f6221ce59106910c090011d0eb1148f0882..febc1a39305c6c4a5f182437bf75e8a3053cb287 100644 --- a/packages/component-invite/src/controllers/assignCollectionRole.js +++ b/packages/component-invite/src/controllers/assignCollectionRole.js @@ -76,10 +76,16 @@ module.exports = async ( ) await collHelper.addAssignedPeople(collection, user, role) } else { - const matchingInvitation = inviteHelper.getMatchingInvitation( - user.invitations, - collectionId, - role, + // const matchingInvitation = inviteHelper.getMatchingInvitation( + // user.invitations, + // collectionId, + // role, + // ) + const matchingInvitation = user.invitations.find( + invitation => + invitation.collectionId === collectionId && + invitation.role === role && + invitation.hasAnswer === false, ) if (matchingInvitation === undefined) { user = await inviteHelper.setupInvitation( diff --git a/packages/component-invite/src/routes/postHandleInvitation.js b/packages/component-invite/src/routes/postHandleInvitation.js index 605e4fb0c76ebf6651387ca275d3a938a39c28b0..a9ef5f0521705caa251d935ce06ff3c4269796bd 100644 --- a/packages/component-invite/src/routes/postHandleInvitation.js +++ b/packages/component-invite/src/routes/postHandleInvitation.js @@ -40,13 +40,12 @@ module.exports = models => async (req, res) => { const matchingInvitation = matchingInvitations[0] matchingInvitation.hasAnswer = true + const users = await models.User.all() + const eic = users.find(user => user.editorInChief === true) if (accept === true) { matchingInvitation.isAccepted = true await collectionHelper.updateAssignedPeople(collection, user.email) try { - const users = await models.User.all() - - const eic = users.find(user => user.editorInChief === true) await mailService.setupHandlingEditorAgreedEmail( eic.email, user, @@ -73,6 +72,18 @@ module.exports = models => async (req, res) => { if (reason !== undefined) { matchingInvitation.reason = reason } + try { + await mailService.setupDeclineEmail( + eic.email, + user, + 'handling-editor-declined', + collection.customId, + reason, + ) + } catch (e) { + logger.error(e) + return res.status(500).json({ error: 'Mail could not be sent.' }) + } } user = await user.save() res.status(200).json(user) diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-invite/src/tests/fixtures/users.js index c1542563fd1b62c510a94705386cb06be944c341..b2df443684be78b5c88a92fcde5bdd2fefd03a9a 100644 --- a/packages/component-invite/src/tests/fixtures/users.js +++ b/packages/component-invite/src/tests/fixtures/users.js @@ -111,7 +111,7 @@ const users = { }, ], teams: [heTeamID], - save: jest.fn(() => users.handlingEditor), + save: jest.fn(() => users.invitedHandlingEditor), editorInChief: false, }, } diff --git a/packages/component-invite/src/tests/postHandleInvitation.test.js b/packages/component-invite/src/tests/postHandleInvitation.test.js index 68bec23c38908eec9f2bedf7755b730ea66b96d0..0bf628cf4e0d902f560741b47e364c29e43f3b6b 100644 --- a/packages/component-invite/src/tests/postHandleInvitation.test.js +++ b/packages/component-invite/src/tests/postHandleInvitation.test.js @@ -9,6 +9,7 @@ const models = Model.build() jest.mock('pubsweet-component-mail-service', () => ({ setupAssignEmail: jest.fn(), setupHandlingEditorAgreedEmail: jest.fn(), + setupDeclineEmail: jest.fn(), })) const notFoundError = new Error() diff --git a/packages/component-invite/src/tests/postInvite.test.js b/packages/component-invite/src/tests/postInvite.test.js index fce9d62a45e5e41f4ccbfd1b7622ab6c9f3e61bf..76720c1e3049a0c42d2bc3456437bbe421098c11 100644 --- a/packages/component-invite/src/tests/postInvite.test.js +++ b/packages/component-invite/src/tests/postInvite.test.js @@ -14,6 +14,7 @@ const models = Model.build() jest.mock('pubsweet-component-mail-service', () => ({ setupInviteEmail: jest.fn(), setupAssignEmail: jest.fn(), + setupDeclineEmail: jest.fn(), })) const chance = new Chance() const globalRoles = configRoles.global @@ -218,6 +219,26 @@ describe('Post invite route handler', () => { const data = JSON.parse(res._getData()) expect(data.email).toEqual(body.email) expect(data.invitations[0].collectionId).toEqual(req.params.collectionId) - expect(data.invitations).toHaveLength(1) + }) + it('should return success when the EiC invites the a HE after he declined an invitation', async () => { + const body = { + email: invitedHandlingEditor.email, + role: 'handlingEditor', + } + const req = httpMocks.createRequest({ + body, + }) + req.user = editorInChief.id + req.params.collectionId = standardCollection.id + const initialSize = invitedHandlingEditor.invitations.length + const res = httpMocks.createResponse() + await require(postInvitePath)(models)(req, res) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.email).toEqual(body.email) + expect(invitedHandlingEditor.invitations.length).toBeGreaterThan( + initialSize, + ) }) }) diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index f7f573102cce617530cb6732e5ae1c1b06aa6448..005b1a01ccffcdfc674ca97e5ce6725ecd1b25f7 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -104,6 +104,28 @@ module.exports = { } return Email.send(mailData) }, + setupDeclineEmail: async (toEmail, user, emailType, collectionId, reason) => { + let finalReason = '' + if (reason !== undefined) { + finalReason = `Reason: "${reason}"` + } + const replacements = { + finalReason, + name: `${user.firstName} ${user.lastName}`, + collectionId, + } + + const { htmlBody, textBody } = getEmailBody(emailType, replacements) + const mailData = { + from: config.get('mailer.from'), + to: toEmail, + subject: 'Handling Editor Declined', + text: textBody, + html: htmlBody, + } + + return Email.send(mailData) + }, } const getEmailBody = (emailType, replacements) => { diff --git a/packages/component-mail-service/src/templates/handling-editor-declined.html b/packages/component-mail-service/src/templates/handling-editor-declined.html new file mode 100644 index 0000000000000000000000000000000000000000..0c7da04e3cbd2ad0e9fa52ae0919c08bb0ac7657 --- /dev/null +++ b/packages/component-mail-service/src/templates/handling-editor-declined.html @@ -0,0 +1,210 @@ +<!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>handling editor invitation declined</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 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff"> + <h1 style="text-align: center;">Handling Editor Invitation Declined</h1> + + <div style="text-align: center;">{{ name }} has declined to be Handling Editor on manuscript <span style="font-family:courier,monospace;">{{ collectionId }}</span>.</div> + <div style="text-align: center;">{{ finalReason }}</div> + <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/handling-editor-declined.txt b/packages/component-mail-service/src/templates/handling-editor-declined.txt new file mode 100644 index 0000000000000000000000000000000000000000..005491da1edb73d2ee8453f3201d5e01176d8a67 --- /dev/null +++ b/packages/component-mail-service/src/templates/handling-editor-declined.txt @@ -0,0 +1,6 @@ +Handling Editor Invitation Declined +{{ name }} has declined to be Handling Editor on manuscript {{ collectionId }}. +{{ finalReason }} +Hindawi Publishing Corporation +315 Madison Ave, Third Floor, Suite 307 +New York, NY 10017 \ No newline at end of file diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index d95dc0c258b2bef26cec1de6e0b5ea6d15286e9d..02e08e23d5e4b773f8cd033f5e5d3bdff7f15d40 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -164,6 +164,7 @@ export default compose( const assignedHE = assignedPeople && assignedPeople.find(p => p.role === 'handlingEditor') + // this can be changed, but it works; cba if (isAdmin || isEic) { if (status === 'submitted' || status === 'he-invited') return <EditorInChiefActions project={project} /> diff --git a/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js b/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js index 620ad1c7a7a1889df6149ff67e98ef593c1b44dd..34eb2d492ebd8482b075195f88b4aa128eac2d8c 100644 --- a/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js +++ b/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js @@ -11,6 +11,7 @@ import { SuccessModal, } from 'pubsweet-component-modal/src/components' +import { handleError } from './utils' import { revokeHandlingEditor, assignHandlingEditor } from '../../redux/editors' import HEModal from './AssignHEModal' @@ -25,12 +26,12 @@ const EditorInChiefActions = ({ const handlingEditor = getHandlingEditor() return ( <Root> - <HEActions> + <HEActions data-test="eic-assign"> {handlingEditor ? ( <HEActions> <HEName>{get(handlingEditor, 'name')}</HEName> {!handlingEditor.hasAnswer && ( - <HEActions> + <HEActions data-test="eic-after-assign"> <div onClick={showConfirmModal('resend')}> <Icon color={theme.colorPrimary}>refresh-cw</Icon> </div> @@ -60,10 +61,6 @@ const CardModal = ({ type, ...rest }) => { } } -const handleError = fn => e => { - fn(get(JSON.parse(e.response), 'error') || 'Oops! Something went wrong!') -} - export default compose( connect(null, { revokeHandlingEditor, @@ -183,9 +180,9 @@ const HEActions = styled.div` const AssignButton = styled(Button)` ${defaultText}; align-items: center; - background-color: ${th('colorPrimary')}; color: ${th('colorTextReverse')}; - text-align: center; + background-color: ${th('colorPrimary')}; height: calc(${th('subGridUnit')}*5); + text-align: center; ` // #endregion diff --git a/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js b/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js index 6da2664f874eb242c1bc33273fe058b9fd0972d0..5064e6aef7cbe80d3b438340bb1c5775467f250d 100644 --- a/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js +++ b/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js @@ -1,14 +1,15 @@ import React from 'react' import { connect } from 'react-redux' -import styled from 'styled-components' -import { th, Button } from '@pubsweet/ui' import { actions } from 'pubsweet-client' +import { th, Button } from '@pubsweet/ui' +import styled, { css } from 'styled-components' import { withHandlers, compose, withState } from 'recompose' import { withModal, ConfirmationModal, } from 'pubsweet-component-modal/src/components' +import { handleError } from './utils' import { handlingEditorDecision } from '../../redux/editors' const DeclineModal = compose( @@ -26,7 +27,7 @@ const DeclineModal = compose( placeholder="Decline reason (optional)" value={reason} /> - <div> + <div data-test="he-buttons"> <Button onClick={hideModal}>Cancel</Button> <Button onClick={onConfirm(reason)} primary> Decline @@ -44,10 +45,10 @@ const ModalComponent = ({ type, ...rest }) => const HandlingEditorActions = ({ showHEModal }) => ( <Root> - <Button onClick={showHEModal('decline')}>DECLINE</Button> - <Button onClick={showHEModal()} primary> + <DecisionButton onClick={showHEModal('decline')}>DECLINE</DecisionButton> + <DecisionButton onClick={showHEModal()} primary> AGREE - </Button> + </DecisionButton> </Root> ) @@ -67,6 +68,7 @@ export default compose( project, getCollections, updateCollection, + setModalError, }) => modalType => { const agreeConfig = { type: modalType, @@ -81,7 +83,7 @@ export default compose( getCollections() hideModal() }) - }, hideModal) + }, handleError(setModalError)) }, } const declineConfig = { @@ -97,7 +99,7 @@ export default compose( getCollections() hideModal() }) - }, hideModal) + }, handleError(setModalError)) }, } return () => { @@ -109,6 +111,22 @@ export default compose( )(HandlingEditorActions) // #region styled-components +const defaultText = css` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` + +const DecisionButton = styled(Button)` + ${defaultText}; + align-items: center; + color: ${({ primary }) => + primary ? th('colorTextReverse') : th('colorPrimary')}); + background-color: ${({ primary }) => + primary ? th('colorPrimary') : th('backgroundColorReverse')}; + height: calc(${th('subGridUnit')}*5); + text-align: center; +` + const DeclineRoot = styled.div` align-items: center; background-color: ${th('backgroundColor')}; diff --git a/packages/components-faraday/src/components/Dashboard/utils.js b/packages/components-faraday/src/components/Dashboard/utils.js index 3e0bc448f84242dd9068eb96f5c42f095938990b..6a006693279d0bf54d5b2ca48a670e7253323f8e 100644 --- a/packages/components-faraday/src/components/Dashboard/utils.js +++ b/packages/components-faraday/src/components/Dashboard/utils.js @@ -1,5 +1,5 @@ -import { get, isEmpty, forEach, isArray, find } from 'lodash' import moment from 'moment' +import { get, isEmpty, forEach, isArray, find } from 'lodash' export const parseTitle = version => { const title = get(version, 'metadata.title') @@ -87,3 +87,7 @@ export const mapStatusToLabel = status => { return 'Draft' } } + +export const handleError = fn => e => { + fn(get(JSON.parse(e.response), 'error') || 'Oops! Something went wrong!') +}