diff --git a/packages/component-invite/src/controllers/assignCollectionRole.js b/packages/component-invite/src/controllers/assignCollectionRole.js index e810e941147c908584f9b5ec5daaf72aa9c2ad5a..cd855f6221ce59106910c090011d0eb1148f0882 100644 --- a/packages/component-invite/src/controllers/assignCollectionRole.js +++ b/packages/component-invite/src/controllers/assignCollectionRole.js @@ -18,14 +18,6 @@ module.exports = async ( url, resend, ) => { - if (reqUser.admin) { - logger.error(`admin tried to invite a ${role} to a collection`) - - return res.status(403).json({ - error: `admin cannot invite an ${role} to a collection`, - }) - } - if (!configRoles.collection.includes(role)) { logger.error(`invitation has been attempted with invalid role: ${role}`) return res @@ -33,11 +25,11 @@ module.exports = async ( .json({ error: `Role ${role} cannot be set on collections` }) } - if (!reqUser.editorInChief && reqUser.teams === undefined) { + if (!reqUser.editorInChief && reqUser.teams === undefined && !reqUser.admin) { return res .status(403) .json({ error: `User ${reqUser.username} is not part of any teams` }) - } else if (reqUser.editorInChief === false) { + } else if (reqUser.editorInChief === false && reqUser.admin === false) { const matchingTeams = await teamHelper.getMatchingTeams( reqUser.teams, models.Team, @@ -67,24 +59,13 @@ module.exports = async ( try { let user = await models.User.findByEmail(email) - let team = await teamHelper.getTeamByGroupAndCollection( + const team = await teamHelper.setupManuscriptTeam( + models, + user, collectionId, role, - models.Team, ) - if (team === undefined) { - team = await teamHelper.setupManuscriptTeam( - models, - user, - collectionId, - role, - ) - user = await models.User.findByEmail(email) - } else { - user.teams = user.teams || [] - user.teams.push(team.id) - user = await user.save() - } + user = await models.User.findByEmail(email) if (user.invitations === undefined) { user = await inviteHelper.setupInvitation( diff --git a/packages/component-invite/src/helpers/Collection.js b/packages/component-invite/src/helpers/Collection.js index d11dc8b7011c1fe50c5b9b7aae4d288a16eae402..da805647f2381691090bc339e83badde97c35073 100644 --- a/packages/component-invite/src/helpers/Collection.js +++ b/packages/component-invite/src/helpers/Collection.js @@ -19,4 +19,11 @@ module.exports = { collection.assignedPeople.push(assignedPerson) await collection.save() }, + removeAssignedPeople: async (collection, email) => { + const assignedPeople = collection.assignedPeople.filter( + person => person.email !== email, + ) + collection.assignedPeople = assignedPeople + await collection.save() + }, } diff --git a/packages/component-invite/src/helpers/Team.js b/packages/component-invite/src/helpers/Team.js index 39bbca5152ab564cbc4e5ac53de18953a962de4a..9a9319e25d942c33b65c0a13053a1e0868871989 100644 --- a/packages/component-invite/src/helpers/Team.js +++ b/packages/component-invite/src/helpers/Team.js @@ -87,7 +87,7 @@ const setupManuscriptTeam = async (models, user, collectionId, role) => { team.members.push(user.id) try { - team = await team.updateProperties(team) + // team = await team.updateProperties(team) team = await team.save() user.teams.push(team.id) await user.save() diff --git a/packages/component-invite/src/routes/deleteInvitation.js b/packages/component-invite/src/routes/deleteInvitation.js index d8c77d72d2d0717ae32a44291a97f2e33aec5ca2..9062262829d0c6ff875bde722f6540feedb84758 100644 --- a/packages/component-invite/src/routes/deleteInvitation.js +++ b/packages/component-invite/src/routes/deleteInvitation.js @@ -4,6 +4,7 @@ const config = require('config') const inviteHelper = require('../helpers/Invitation') const mailService = require('pubsweet-component-mail-service') const logger = require('@pubsweet/logger') +const collectionHelper = require('../helpers/Collection') const configRoles = config.get('roles') module.exports = models => async (req, res) => { @@ -26,7 +27,7 @@ module.exports = models => async (req, res) => { const { collectionId, userId } = req.params try { - await models.Collection.find(collectionId) + const collection = await models.Collection.find(collectionId) let user = await models.User.find(userId) const team = await teamHelper.getTeamByGroupAndCollection( collectionId, @@ -43,13 +44,14 @@ module.exports = models => async (req, res) => { await inviteHelper.revokeInvitation(user, collectionId, role) user = await models.User.find(userId) await teamHelper.removeTeamMember(team.id, userId, models.Team) + await collectionHelper.removeAssignedPeople(collection, user.email) try { await mailService.setupRevokeInvitationEmail( user.email, 'revoke-handling-editor', ) - return res.status(204).json() + return res.status(200).json({}) } catch (e) { logger.error(e.message) return res.status(500).json({ error: 'Email could not be sent.' }) diff --git a/packages/component-invite/src/routes/postHandleInvitation.js b/packages/component-invite/src/routes/postHandleInvitation.js index 85fd4151dcd1e929cc6f95b7cafd4435f958c482..5be9650861cc50aed18fffd6ed959da624936792 100644 --- a/packages/component-invite/src/routes/postHandleInvitation.js +++ b/packages/component-invite/src/routes/postHandleInvitation.js @@ -1,6 +1,7 @@ const logger = require('@pubsweet/logger') const helpers = require('../helpers/helpers') const teamHelper = require('../helpers/Team') +const mailService = require('pubsweet-component-mail-service') module.exports = models => async (req, res) => { const { type, accept } = req.body @@ -11,7 +12,7 @@ module.exports = models => async (req, res) => { return } - const user = await models.User.find(req.user) + let user = await models.User.find(req.user) if (!user.invitations) { res.status(400).json({ error: 'The user has no invitation' }) logger.error('The request user does not have any invitation') @@ -20,13 +21,13 @@ module.exports = models => async (req, res) => { const { collectionId } = req.params try { - await models.Collection.find(collectionId) - const filteredInvitations = user.invitations.filter( + const collection = await models.Collection.find(collectionId) + const matchingInvitations = user.invitations.filter( invitation => invitation.collectionId === collectionId && invitation.type === type, ) - if (filteredInvitations.length === 0) { + if (matchingInvitations.length === 0) { res.status(400).json({ error: `Request data does not match any user invitation`, }) @@ -36,20 +37,39 @@ module.exports = models => async (req, res) => { return } - const matchingInvitation = filteredInvitations[0] + const matchingInvitation = matchingInvitations[0] matchingInvitation.hasAnswer = true if (accept === true) { matchingInvitation.isAccepted = true - await user.save() + try { + const users = await models.User.all() + + const eic = users.find(user => user.editorInChief === true) + await mailService.setupHandlingEditorAgreedEmail( + eic.email, + user, + 'handling-editor-agreed', + `${req.protocol}://${req.get('host')}`, + collection.customId, + ) + } catch (e) { + logger.error(e) + return res.status(500).json({ error: 'Mail could not be sent.' }) + } } else { + matchingInvitation.isAccepted = false await teamHelper.removeTeamMember( matchingInvitation.teamId, user.id, models.Team, ) + const { reason } = req.body + if (reason !== undefined) { + matchingInvitation.reason = reason + } } - - res.status(204).json() + user = await user.save() + res.status(200).json(user) return } catch (e) { const notFoundError = await helpers.handleNotFoundError(e, 'collection') diff --git a/packages/component-invite/src/tests/deleteInvitation.test.js b/packages/component-invite/src/tests/deleteInvitation.test.js index 30e994b12abf5365195570f6d48892815709e6f7..e20a0773af872f4105b4aa171588124a2415ca1a 100644 --- a/packages/component-invite/src/tests/deleteInvitation.test.js +++ b/packages/component-invite/src/tests/deleteInvitation.test.js @@ -27,7 +27,7 @@ describe('Delete Invitation route handler', () => { const res = httpMocks.createResponse() const initialSize = handlingEditor.invitations.length await require(deleteInvitationPath)(models)(req, res) - expect(res.statusCode).toBe(204) + expect(res.statusCode).toBe(200) expect(heTeam.members).not.toContain(handlingEditor.id) expect(handlingEditor.invitations).toHaveLength(initialSize - 1) }) diff --git a/packages/component-invite/src/tests/fixtures/collections.js b/packages/component-invite/src/tests/fixtures/collections.js index 210aa318228faea749cc790593ec29bb8a5088d4..09b4b6eb797d61b15e529b247b5e76161736859c 100644 --- a/packages/component-invite/src/tests/fixtures/collections.js +++ b/packages/component-invite/src/tests/fixtures/collections.js @@ -1,4 +1,5 @@ const Chance = require('chance') +const { handlingEditor } = require('./userData') const chance = new Chance() module.exports = { @@ -9,6 +10,16 @@ module.exports = { fragments: [], owners: [], save: jest.fn(), + assignedPeople: [ + { + id: handlingEditor.id, + name: `${handlingEditor.firstName} ${handlingEditor.lastName}`, + role: 'handlingEditor', + email: handlingEditor.email, + hasAnswer: false, + isAccepted: false, + }, + ], }, noTeamCollection: { id: chance.guid(), diff --git a/packages/component-invite/src/tests/fixtures/userData.js b/packages/component-invite/src/tests/fixtures/userData.js new file mode 100644 index 0000000000000000000000000000000000000000..a4a2327911e9969621d34315b0b4b0ad9176fdbf --- /dev/null +++ b/packages/component-invite/src/tests/fixtures/userData.js @@ -0,0 +1,12 @@ +const Chance = require('chance') + +const chance = new Chance() + +module.exports = { + handlingEditor: { + id: chance.guid(), + email: chance.email(), + firstName: chance.first(), + lastName: chance.last(), + }, +} diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-invite/src/tests/fixtures/users.js index 53e00485ec4413381309488acb126c2ccb4b2dfe..c1542563fd1b62c510a94705386cb06be944c341 100644 --- a/packages/component-invite/src/tests/fixtures/users.js +++ b/packages/component-invite/src/tests/fixtures/users.js @@ -1,5 +1,6 @@ const { standardCollection } = require('./collections') const { heTeamID, reviewerTeamID } = require('./teamIDs') +const { handlingEditor } = require('./userData') const users = { admin: { @@ -29,12 +30,12 @@ const users = { handlingEditor: { type: 'user', username: 'handling', - email: 'handling@example.com', + email: handlingEditor.email, password: 'test', admin: false, - id: 'handling123', - firstName: 'Handling', - lastName: 'Editor', + id: handlingEditor.id, + firstName: handlingEditor.firstName, + lastName: handlingEditor.lastName, invitations: [ { type: 'handlingEditor', diff --git a/packages/component-invite/src/tests/helpers/Model.js b/packages/component-invite/src/tests/helpers/Model.js index 7ff2e90b59f218c2e37967701d0ea8b70d78c73b..420cc704e1cfb10eaeb4e346648c192054f7b0dc 100644 --- a/packages/component-invite/src/tests/helpers/Model.js +++ b/packages/component-invite/src/tests/helpers/Model.js @@ -16,9 +16,8 @@ const build = () => { Team: {}, } UserMock.find = jest.fn(id => findMock(id, 'users')) - UserMock.findByEmail = jest.fn(email => findByEmailMock(email)) - + UserMock.all = jest.fn(() => Object.values(fixtures.users)) TeamMock.find = jest.fn(id => findMock(id, 'teams')) TeamMock.updateProperties = jest.fn(team => updatePropertiesMock(team, 'teams'), diff --git a/packages/component-invite/src/tests/postHandleInvitation.test.js b/packages/component-invite/src/tests/postHandleInvitation.test.js index ffa1215761d1b0a017f09a71f939dacb2d2a1c3c..68bec23c38908eec9f2bedf7755b730ea66b96d0 100644 --- a/packages/component-invite/src/tests/postHandleInvitation.test.js +++ b/packages/component-invite/src/tests/postHandleInvitation.test.js @@ -8,6 +8,7 @@ const Model = require('./helpers/Model') const models = Model.build() jest.mock('pubsweet-component-mail-service', () => ({ setupAssignEmail: jest.fn(), + setupHandlingEditorAgreedEmail: jest.fn(), })) const notFoundError = new Error() @@ -36,7 +37,7 @@ describe('Post handle invitation route handler', () => { req.params.collectionId = standardCollection.id const res = httpMocks.createResponse() await require(postInvitationPath)(models)(req, res) - expect(res.statusCode).toBe(204) + expect(res.statusCode).toBe(200) expect(handlingEditor.invitations[0].hasAnswer).toBeTruthy() expect(handlingEditor.invitations[0].isAccepted).toBeTruthy() }) @@ -53,7 +54,7 @@ describe('Post handle invitation route handler', () => { const res = httpMocks.createResponse() await require(postInvitationPath)(models)(req, res) - expect(res.statusCode).toBe(204) + expect(res.statusCode).toBe(200) expect(invitedHandlingEditor.invitations[0].hasAnswer).toBeTruthy() expect(invitedHandlingEditor.invitations[0].isAccepted).toBeFalsy() }) diff --git a/packages/component-invite/src/tests/postInvite.test.js b/packages/component-invite/src/tests/postInvite.test.js index e350f5c33c485da065b6d4e029e9aa59fd06ad3c..fce9d62a45e5e41f4ccbfd1b7622ab6c9f3e61bf 100644 --- a/packages/component-invite/src/tests/postInvite.test.js +++ b/packages/component-invite/src/tests/postInvite.test.js @@ -39,6 +39,7 @@ const { invitedHandlingEditor, } = fixtures.users const { standardCollection } = fixtures.collections +const { heTeam } = fixtures.teams const postInvitePath = '../routes/postInvite' describe('Post invite route handler', () => { it('should return success when the admin invites a global role', async () => { @@ -55,36 +56,6 @@ describe('Post invite route handler', () => { expect(data.email).toEqual(body.email) expect(data.admin).toEqual(body.admin) }) - it('should return an error when the admin invites a user on a collection', async () => { - const req = httpMocks.createRequest({ - body, - }) - req.user = admin.id - req.params.collectionId = '123' - const res = httpMocks.createResponse() - await require(postInvitePath)(models)(req, res) - expect(res.statusCode).toBe(403) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual( - `admin cannot invite an ${body.role} to a collection`, - ) - }) - it('should return an error when the admin invites a reviewer', async () => { - body.role = 'reviewer' - body.admin = false - const req = httpMocks.createRequest({ - body, - }) - req.user = admin.id - const res = httpMocks.createResponse() - await require(postInvitePath)(models)(req, res) - expect(res.statusCode).toBe(403) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual( - `admin tried to invite an invalid role: ${body.role}`, - ) - body.role = globalRoles[random(0, globalRoles.length - 1)] - }) it('should return an error params are missing', async () => { delete body.email const req = httpMocks.createRequest({ @@ -171,6 +142,7 @@ describe('Post invite route handler', () => { body, }) req.user = editorInChief.id + const initialSize = standardCollection.assignedPeople.length req.params.collectionId = standardCollection.id const res = httpMocks.createResponse() await require(postInvitePath)(models)(req, res) @@ -179,7 +151,11 @@ 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(standardCollection.assignedPeople).toHaveLength(1) + expect(standardCollection.assignedPeople.length).toBeGreaterThan( + initialSize, + ) + expect(heTeam.members).toContain(author.id) + expect(author.teams).toContain(heTeam.id) }) it('should return success when the handlingEditor invites a reviewer with a collection', async () => { const body = { diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index 11d1f29f20684c90abf2c8e8f0df3e2f3baeb2fc..f7f573102cce617530cb6732e5ae1c1b06aa6448 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -83,6 +83,27 @@ module.exports = { } return Email.send(mailData) }, + setupHandlingEditorAgreedEmail: async ( + toEmail, + user, + emailType, + url, + collectionId, + ) => { + const { htmlBody, textBody } = getEmailBody(emailType, { + url, + name: `${user.firstName} ${user.lastName}`, + collectionId, + }) + const mailData = { + from: config.get('mailer.from'), + to: toEmail, + subject: 'Handling Editor Agreed', + text: textBody, + html: htmlBody, + } + return Email.send(mailData) + }, } const getEmailBody = (emailType, replacements) => { diff --git a/packages/component-mail-service/src/templates/handling-editor-agreed.html b/packages/component-mail-service/src/templates/handling-editor-agreed.html new file mode 100644 index 0000000000000000000000000000000000000000..3f24b1f4216f3d8554def6a0bc63fc80fb4afa36 --- /dev/null +++ b/packages/component-mail-service/src/templates/handling-editor-agreed.html @@ -0,0 +1,232 @@ +<!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 agreed</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;">{{ name }} has agreed to be Handling Editor on manuscript {{ collectionId }}</h1> + + <div style="text-align: center;">Please click on the link below to access your dashboard.</div> + + <div style="text-align: center;"> </div> + + <div style="text-align: center;"> </div> + + </td> + </tr> + </table> + <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" bgcolor="#FFFFFF" class="outer-td" style="padding:0px 0px 30px 0px;background-color:#FFFFFF"> + <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 href="{{ url }}" 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" + target="_blank">VIEW DASHBOARD</a> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </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-agreed.txt b/packages/component-mail-service/src/templates/handling-editor-agreed.txt new file mode 100644 index 0000000000000000000000000000000000000000..550581b4489de9dca0da675eb4c3da2db498d815 --- /dev/null +++ b/packages/component-mail-service/src/templates/handling-editor-agreed.txt @@ -0,0 +1,6 @@ +{{ name }} has agreed to be Handling Editor on manuscript {{ collectionId }} +Please click on the link below to access your dashboard +{{ url }} VIEW DASHBOARD +Hindawi Publishing Corporation +315 Madison Ave, Third Floor, Suite 307 +New York, NY 10017 \ No newline at end of file diff --git a/packages/component-modal/src/components/ConfirmationModal.js b/packages/component-modal/src/components/ConfirmationModal.js index ddc1d30f39d44b94f2a287690cc768e33f804241..7d1b6664396f7396d6ecfa8cb17d19464f743660 100644 --- a/packages/component-modal/src/components/ConfirmationModal.js +++ b/packages/component-modal/src/components/ConfirmationModal.js @@ -1,5 +1,5 @@ import React from 'react' -import { Icon, Button } from '@pubsweet/ui' +import { Icon, Button, th } from '@pubsweet/ui' import styled, { css, withTheme } from 'styled-components' const ConfirmationModal = ({ @@ -11,6 +11,7 @@ const ConfirmationModal = ({ cancelText = 'Cancel', hideModal, theme, + modalError, }) => ( <Root> <CloseIcon data-test="icon-modal-hide" onClick={hideModal}> @@ -19,13 +20,18 @@ const ConfirmationModal = ({ {title && <Title dangerouslySetInnerHTML={{ __html: title }} />} {subtitle && <Subtitle dangerouslySetInnerHTML={{ __html: subtitle }} />} {content && <Content dangerouslySetInnerHTML={{ __html: content }} />} + + {modalError && <ErrorMessage>{modalError}</ErrorMessage>} + <ButtonsContainer> <Button data-test="button-modal-hide" onClick={hideModal}> {cancelText} </Button> - <Button data-test="button-modal-confirm" onClick={onConfirm} primary> - {confirmText} - </Button> + {onConfirm && ( + <Button data-test="button-modal-confirm" onClick={onConfirm} primary> + {confirmText} + </Button> + )} </ButtonsContainer> </Root> ) @@ -34,51 +40,59 @@ export default withTheme(ConfirmationModal) // #region styled-components const defaultText = css` - color: ${({ theme }) => theme.colorText}; - font-family: ${({ theme }) => theme.fontReading}; - font-size: ${({ theme }) => theme.fontSizeBaseSmall}; + color: ${th('colorText')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; ` const Root = styled.div` - background-color: ${({ theme }) => theme.backgroundColor}; - padding: 50px 32px 32px 32px; - border: ${({ theme }) => theme.borderDefault}; + background-color: ${th('backgroundColor')}; + border: ${th('borderDefault')}; + display: flex; + flex-direction: column; + justify-content: space-between; + max-height: calc(${th('gridUnit')} * 20); + padding: calc(${th('gridUnit')} * 2); position: relative; - width: 600px; - max-height: 500px; overflow-y: scroll; + width: calc(${th('gridUnit')} * 25); ` const Title = styled.div` ${defaultText}; - font-size: ${({ theme }) => theme.fontSizeBase}; + font-size: ${th('fontSizeHeading5')}; + margin-bottom: ${th('gridUnit')}; text-align: center; - margin-bottom: 20px; ` const Subtitle = styled.div` ${defaultText}; - font-weight: bold; - line-height: 1.57; - margin-bottom: 15px; + margin-bottom: calc(${th('subGridUnit')} * 6); text-align: center; ` const Content = styled.div` ${defaultText}; - line-height: 1.57; - margin-top: 10px; + margin-top: calc(${th('subGridUnit')} * 2); text-align: left; ` const ButtonsContainer = styled.div` display: flex; justify-content: space-evenly; - margin: 30px auto 0; + margin: ${th('gridUnit')} auto 0; + width: 100%; ` const CloseIcon = styled.div` cursor: pointer; position: absolute; - top: 5px; - right: 5px; + top: ${th('subGridUnit')}; + right: ${th('subGridUnit')}; ` + +const ErrorMessage = styled.div` + color: ${th('colorError')}; + margin: ${th('subGridUnit')}; + text-align: center; +` + // #endregion diff --git a/packages/component-modal/src/components/Modal.js b/packages/component-modal/src/components/Modal.js index 4015782fef9d60d7ecde1e2d19efeb83628036d5..b16774202a19629f580e8fe050200b9c1fa2ba8c 100644 --- a/packages/component-modal/src/components/Modal.js +++ b/packages/component-modal/src/components/Modal.js @@ -40,5 +40,5 @@ const ModalRoot = styled.div` justify-content: center; background-color: ${({ overlayColor }) => overlayColor || 'rgba(0, 0, 0, 0.8)'}; - /* z-index: ${({ theme }) => theme.modalIndex}; */ + z-index: ${({ theme }) => theme.modalIndex}; ` diff --git a/packages/component-modal/src/components/SuccessModal.js b/packages/component-modal/src/components/SuccessModal.js new file mode 100644 index 0000000000000000000000000000000000000000..676ba603e6cd5312967d6d6a9efa548fb85c1dc4 --- /dev/null +++ b/packages/component-modal/src/components/SuccessModal.js @@ -0,0 +1,51 @@ +import React from 'react' +import { Button, th } from '@pubsweet/ui' +import styled, { css, withTheme } from 'styled-components' + +const SuccessModal = ({ title, confirmText = 'OK', hideModal, theme }) => ( + <Root> + {title && <Title dangerouslySetInnerHTML={{ __html: title }} />} + <ButtonsContainer> + <Button data-test="button-modal-confirm" onClick={hideModal} primary> + {confirmText} + </Button> + </ButtonsContainer> + </Root> +) + +export default withTheme(SuccessModal) + +// #region styled-components +const defaultText = css` + color: ${th('colorText')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` + +const Root = styled.div` + background-color: ${th('backgroundColor')}; + border: ${th('borderDefault')}; + display: flex; + flex-direction: column; + justify-content: space-between; + max-height: calc(${th('gridUnit')} * 20); + padding: calc(${th('gridUnit')} * 2); + position: relative; + overflow-y: scroll; + width: calc(${th('gridUnit')} * 25); +` + +const Title = styled.div` + ${defaultText}; + font-size: ${th('fontSizeHeading5')}; + margin-bottom: ${th('gridUnit')}; + text-align: center; +` + +const ButtonsContainer = styled.div` + display: flex; + justify-content: space-evenly; + margin: ${th('gridUnit')} auto 0; + width: 100%; +` +// #endregion diff --git a/packages/component-modal/src/components/index.js b/packages/component-modal/src/components/index.js index 22f1e90fca99f0f6b8c1d66c3d91272730d9078d..724e8d8878f79f7754077b39dda7a1105632eebb 100644 --- a/packages/component-modal/src/components/index.js +++ b/packages/component-modal/src/components/index.js @@ -1,2 +1,3 @@ export { default as withModal } from './withModal' +export { default as SuccessModal } from './SuccessModal' export { default as ConfirmationModal } from './ConfirmationModal' diff --git a/packages/component-modal/src/components/withModal.js b/packages/component-modal/src/components/withModal.js index 1d5c786c7b6026264442f30a9ab52ecb59c0a7fa..84a7b51fd1a957730e963cde62d78db750082b87 100644 --- a/packages/component-modal/src/components/withModal.js +++ b/packages/component-modal/src/components/withModal.js @@ -3,16 +3,18 @@ import { omit } from 'lodash' import { connect } from 'react-redux' import Modal from './Modal' -import { showModal, hideModal } from '../redux/modal' +import { showModal, hideModal, setModalError } from '../redux/modal' const mapState = state => ({ modalsVisibility: omit(state.modal, 'props'), modalProps: state.modal.props, + modalError: state.modal.error, }) const mapDispatch = modalKey => (dispatch, propss) => ({ hideModal: () => dispatch(hideModal()), showModal: (modalProps = {}) => dispatch(showModal(modalKey, modalProps)), + setModalError: errorMessage => dispatch(setModalError(errorMessage)), }) const withModal = ({ @@ -21,13 +23,14 @@ const withModal = ({ overlayColor, }) => WrappedComponent => connect(mapState, mapDispatch(modalKey))( - ({ modalsVisibility, modalProps, hideModal, ...rest }) => ( + ({ modalsVisibility, modalProps, modalError, hideModal, ...rest }) => ( <React.Fragment> {modalsVisibility[modalKey] && ( <Modal {...modalProps} component={Component} hideModal={hideModal} + modalError={modalError} overlayColor={overlayColor} /> )} diff --git a/packages/component-modal/src/redux/modal.js b/packages/component-modal/src/redux/modal.js index cba9f707d60c3001c123abad56ced39270e1821e..06d5702bfafc89ef6732a95773a3504490eac45a 100644 --- a/packages/component-modal/src/redux/modal.js +++ b/packages/component-modal/src/redux/modal.js @@ -1,8 +1,10 @@ const initialState = { + error: null, props: {}, } const SHOW_MODAL = 'modal/SHOW_MODAL' +const SET_MODAL_ERROR = 'modal/SET_MODAL_ERROR' const HIDE_MODAL = 'modal/HIDE_MODAL' export const showModal = (modalKey, props = {}) => ({ @@ -13,6 +15,13 @@ export const showModal = (modalKey, props = {}) => ({ }, }) +export const setModalError = error => ({ + type: SET_MODAL_ERROR, + payload: { + error, + }, +}) + export const hideModal = () => ({ type: HIDE_MODAL, }) @@ -24,8 +33,14 @@ export default (state = initialState, action = {}) => { case SHOW_MODAL: return { [action.payload.modalKey]: true, + error: null, props: action.payload.props, } + case SET_MODAL_ERROR: + return { + ...state, + error: action.payload.error, + } case HIDE_MODAL: return initialState default: diff --git a/packages/component-wizard/src/components/WizardFormStep.js b/packages/component-wizard/src/components/WizardFormStep.js index 3e8c0a020bc413beaf229c50ba36cabe3359d536..b50d4d3e408bc85af1d024e385aa8aa350fa8486 100644 --- a/packages/component-wizard/src/components/WizardFormStep.js +++ b/packages/component-wizard/src/components/WizardFormStep.js @@ -8,7 +8,6 @@ import { reduxForm, formValueSelector, SubmissionError } from 'redux-form' import WizardStep from './WizardStep' import { autosaveRequest } from '../redux/autosave' -let cachedVersion = '' const wizardSelector = formValueSelector('wizard') const onChange = ( @@ -20,13 +19,11 @@ const onChange = ( const prev = pick(prevValues, formSectionKeys) const newValues = pick(values, formSectionKeys) // TODO: fix this if it sucks down the road - if (!isEqual(prev, newValues) && cachedVersion !== version.rev) { - cachedVersion = version.rev + if (!isEqual(prev, newValues)) { dispatch(autosaveRequest()) dispatch( actions.updateFragment(project, { id: version.id, - rev: version.rev, ...newValues, }), ) @@ -44,7 +41,6 @@ const submitManuscript = ( dispatch( actions.updateFragment(project, { id: version.id, - rev: version.rev, submitted: new Date(), ...values, }), @@ -53,7 +49,6 @@ const submitManuscript = ( dispatch( actions.updateCollection({ id: project.id, - rev: project.rev, status: 'submitted', }), ), diff --git a/packages/components-faraday/src/components/Admin/AdminUsers.js b/packages/components-faraday/src/components/Admin/AdminUsers.js index 4185c022c7a6c704fdacb35f20039c4d29690789..f81860a85cb51e8d8f51686a3ba3eb69192af4ad 100644 --- a/packages/components-faraday/src/components/Admin/AdminUsers.js +++ b/packages/components-faraday/src/components/Admin/AdminUsers.js @@ -24,6 +24,7 @@ const TableRow = ({ affiliation, isConfirmed, editorInChief, + handlingEditor, admin, roleOptions, }) => ( @@ -36,8 +37,8 @@ const TableRow = ({ <td>{affiliation}</td> <td> <Role>{`Author${isEqual(editorInChief, true) ? ', Editor in Chief' : ''}${ - isEqual(admin, true) ? ', Admin' : '' - }`}</Role> + isEqual(handlingEditor, true) ? ', Handling Editor' : '' + }${isEqual(admin, true) ? ', Admin' : ''}`}</Role> </td> <td> <Tag>{isConfirmed ? 'Confirmed' : 'Invited'}</Tag> diff --git a/packages/components-faraday/src/components/Admin/EditUserForm.js b/packages/components-faraday/src/components/Admin/EditUserForm.js index ecbef0d9aff2bc5ad41ab4706a3da6cf313e5d95..afbe740a1a0b933dddbab781d39cb952753cb560 100644 --- a/packages/components-faraday/src/components/Admin/EditUserForm.js +++ b/packages/components-faraday/src/components/Admin/EditUserForm.js @@ -69,6 +69,17 @@ const EditUserForm = ({ roles, journal, user, error }) => ( )} name="editorInChief" /> + <ValidatedField + component={input => ( + <Checkbox + checked={input.value} + type="checkbox" + {...input} + label="Handling Editor" + /> + )} + name="handlingEditor" + /> <ValidatedField component={input => ( <Checkbox diff --git a/packages/components-faraday/src/components/Admin/utils.js b/packages/components-faraday/src/components/Admin/utils.js index 18d68678757f353480e592d49f094e0e98129b11..eb9b5cec9493be46809ba4bddafabf961eba5db9 100644 --- a/packages/components-faraday/src/components/Admin/utils.js +++ b/packages/components-faraday/src/components/Admin/utils.js @@ -24,6 +24,7 @@ export const parseUpdateUser = values => { 'roles', 'rev', 'editorInChief', + 'handlingEditor', ] return pick(values, valuesToSave) diff --git a/packages/components-faraday/src/components/Dashboard/AssignEditor.js b/packages/components-faraday/src/components/Dashboard/AssignEditor.js deleted file mode 100644 index 4fca2fe52927497f5f0d595dcc4c7a5ea37b77c0..0000000000000000000000000000000000000000 --- a/packages/components-faraday/src/components/Dashboard/AssignEditor.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import styled, { css } from 'styled-components' -import { Button, th } from '@pubsweet/ui' -import { compose, withHandlers } from 'recompose' -import { withModal } from 'pubsweet-component-modal/src/components' - -import HEModal from './AssignHEModal' - -const AssignEditor = ({ assign }) => ( - <ActionButtons onClick={assign}>ASSIGN</ActionButtons> -) - -export default compose( - withModal({ - modalKey: 'assignHEmodal', - modalComponent: HEModal, - }), - withHandlers({ - assign: ({ showModal, collectionId }) => () => { - showModal({ - collectionId, - }) - }, - }), -)(AssignEditor) - -// #region styled-components -const defaultText = css` - color: ${th('colorText')}; - font-family: ${th('fontReading')}; - font-size: ${th('fontSizeBaseSmall')}; -` - -const ActionButtons = styled(Button)` - ${defaultText}; - align-items: center; - background-color: ${th('colorPrimary')}; - color: ${th('colorTextReverse')}; - text-align: center; - height: calc(${th('subGridUnit')}*5); -` -// #endregion diff --git a/packages/components-faraday/src/components/Dashboard/AssignHEModal.js b/packages/components-faraday/src/components/Dashboard/AssignHEModal.js index 86e6d1438c6334a18508077cd7bbcf56b1ff03f9..592a672532c297eee54a7935536514435bc2f17f 100644 --- a/packages/components-faraday/src/components/Dashboard/AssignHEModal.js +++ b/packages/components-faraday/src/components/Dashboard/AssignHEModal.js @@ -1,10 +1,12 @@ /* eslint react/prefer-stateless-function: 0 */ import React from 'react' -import { th } from '@pubsweet/ui' +import { get } from 'lodash' import { compose } from 'recompose' import { connect } from 'react-redux' -import styled from 'styled-components' +import { th, Icon } from '@pubsweet/ui' +import { actions } from 'pubsweet-client' +import styled, { withTheme } from 'styled-components' import { handlingEditors, assignHandlingEditor } from '../../redux/editors' @@ -27,22 +29,54 @@ class AssignHEModal extends React.Component { } assignEditor = email => () => { - const { assignHandlingEditor, collectionId, hideModal } = this.props - assignHandlingEditor(email, collectionId).then(hideModal) + const { + assignHandlingEditor, + collectionId, + showModal, + hideModal, + setModalError, + updateCollection, + getCollections, + } = this.props + assignHandlingEditor(email, collectionId).then( + () => { + updateCollection({ + id: collectionId, + status: 'he-invited', + }).then(() => { + getCollections() + hideModal() + showModal({ + type: 'confirmation', + title: 'Assignation Sent', + cancelText: 'OK', + }) + }) + }, + e => { + setModalError( + get(JSON.parse(e.response), 'error') || 'Oops! Something went wrong!', + ) + }, + ) } render() { const { searchInput } = this.state - const { editors } = this.props + const { editors, hideModal, theme } = this.props const filteredEditors = this.filterEditors(editors) return ( <RootModal> - <button onClick={this.props.hideModal}>CLOSE</button> + <CloseIcon data-test="icon-modal-hide" onClick={hideModal}> + <Icon color={theme.colorPrimary}>x</Icon> + </CloseIcon> <ModalTitle>Assign Handling Editor</ModalTitle> <ModalHeader> <span>HANDLING EDITORS</span> <SearchInput + data-test="he-search" onChange={this.changeInput} + placeholder="Search by name or email" type="text" value={searchInput} /> @@ -58,7 +92,10 @@ class AssignHEModal extends React.Component { <span>{`${firstName} ${lastName}`}</span> <span>{email}</span> </EditorDetails> - <AssignButton onClick={this.assignEditor(email)}> + <AssignButton + data-test={`assign-${email}`} + onClick={this.assignEditor(email)} + > ASSIGN </AssignButton> </SuggestedEditor> @@ -75,11 +112,23 @@ export default compose( state => ({ editors: handlingEditors(state), }), - { assignHandlingEditor }, + { + assignHandlingEditor, + updateCollection: actions.updateCollection, + getCollections: actions.getCollections, + }, ), + withTheme, )(AssignHEModal) // #region styled-components +const CloseIcon = styled.div` + cursor: pointer; + position: absolute; + top: 5px; + right: 5px; +` + const EditorDetails = styled.div` display: flex; flex-direction: column; @@ -100,13 +149,16 @@ const SuggestedEditor = styled.div` const AssignButton = styled.button` align-items: center; - color: ${th('colorTextReverse')}; background-color: ${th('colorPrimary')}; + cursor: pointer; + color: ${th('colorTextReverse')}; display: flex; justify-content: center; + font-size: ${th('fontSizeBaseSmall')}; + font-family: ${th('fontReading')}; height: ${th('gridUnit')}; - width: calc(${th('gridUnit')} * 4); opacity: 0; + width: calc(${th('gridUnit')} * 4); ${SuggestedEditor}:hover & { opacity: 1; @@ -121,6 +173,7 @@ const RootModal = styled.div` justify-content: flex-start; height: calc(${th('gridUnit')} * 18); padding: calc(${th('subGridUnit')} * 8) calc(${th('subGridUnit')} * 6); + position: relative; width: calc(${th('gridUnit')} * 24); ` @@ -139,6 +192,7 @@ const ModalHeader = styled.div` & span { color: ${th('colorPrimary')}; font-size: ${th('fontSizeBase')}; + font-family: ${th('fontReading')}; margin-bottom: ${th('subGridUnit')}; } ` @@ -147,6 +201,11 @@ const SearchInput = styled.input` border: 4px solid gray; height: calc(${th('subGridUnit')} * 5); padding: ${th('subGridUnit')}; + + &:focus, + &:active { + outline: none; + } ` const ScrollContainer = styled.div` diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index 9401acd55ba87aac3e577ecc444865d973eb433d..5a0087a40ee4f16bf8489a2172ef8b5625de19c6 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -1,12 +1,16 @@ import React from 'react' import { get } from 'lodash' import PropTypes from 'prop-types' -import { compose, getContext } from 'recompose' import { Button, Icon, th } from '@pubsweet/ui' import styled, { css, withTheme } from 'styled-components' +import { compose, getContext, withHandlers } from 'recompose' +import { + withModal, + ConfirmationModal, +} from 'pubsweet-component-modal/src/components' import ZipFiles from './ZipFiles' -import { parseVersion, parseJournalIssue } from './utils' +import { parseVersion, parseJournalIssue, mapStatusToLabel } from './utils' import HandlingEditorActions from './HandlingEditorActions' const DashboardCard = ({ @@ -16,7 +20,7 @@ const DashboardCard = ({ version, showAbstractModal, journal, - cancelSubmission, + showConfirmationModal, theme, ...rest }) => { @@ -66,11 +70,11 @@ const DashboardCard = ({ </RightDetails> </Top> <Bottom> - <LeftDetails flex="2"> - <Status>{status}</Status> + <LeftDetails flex="3"> + <Status>{mapStatusToLabel(status)}</Status> <DateField>{submitted || ''}</DateField> </LeftDetails> - <RightDetails flex="5"> + <RightDetails flex="4"> <ManuscriptType title={manuscriptMeta}> {manuscriptMeta} </ManuscriptType> @@ -89,9 +93,9 @@ const DashboardCard = ({ ) : ( <Details data-test="button-cancel-submission" - onClick={cancelSubmission} + onClick={showConfirmationModal} > - Cancel submission + Delete </Details> )} </RightDetails> @@ -139,9 +143,36 @@ const DashboardCard = ({ ) : null } -export default compose(getContext({ journal: PropTypes.object }), withTheme)( - DashboardCard, -) +export default compose( + getContext({ journal: PropTypes.object }), + withTheme, + withModal({ + modalKey: 'cancelManuscript', + modalComponent: ConfirmationModal, + }), + withHandlers({ + showConfirmationModal: ({ + deleteProject, + showModal, + hideModal, + setModalError, + project, + }) => () => { + showModal({ + title: 'Are you sure you want to delete this submission?', + confirmText: 'Delete', + onConfirm: () => { + deleteProject(project).then(hideModal, e => { + setModalError( + get(JSON.parse(e.response), 'error') || + 'Oops! Something went wrong!', + ) + }) + }, + }) + }, + }), +)(DashboardCard) // #region styled-components const defaultText = css` diff --git a/packages/components-faraday/src/components/Dashboard/DashboardPage.js b/packages/components-faraday/src/components/Dashboard/DashboardPage.js index 1e59c3c6684e7284307aeb139d0d7ee0f780971f..00a2fc968658af3bf5384f74a7f91a209b95ec43 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardPage.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardPage.js @@ -10,16 +10,11 @@ import { newestFirst, selectCurrentUser } from 'xpub-selectors' import { createDraftSubmission } from 'pubsweet-component-wizard/src/redux/conversion' import Dashboard from './Dashboard' -import { getHandlingEditors } from '../../redux/editors' import withFilters from './withFilters' +import { getHandlingEditors } from '../../redux/editors' export default compose( - ConnectPage(() => [ - actions.getCollections(), - actions.getTeams(), - actions.getUsers(), - getHandlingEditors(), - ]), + ConnectPage(() => [actions.getCollections()]), connect( state => { const { collections, conversion } = state @@ -50,6 +45,12 @@ export default compose( createDraftSubmission: () => dispatch(createDraftSubmission(history)), }), ), + ConnectPage( + ({ currentUser }) => + get(currentUser, 'admin') || get(currentUser, 'editorInChief') + ? [getHandlingEditors()] + : [], + ), withRouter, withJournal, withFilters({ diff --git a/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js b/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js index c9a60e77aee3acdcdf1bd9ebbd390a808121c61b..33c9eb65b2cc952e6bb6a79b766f6a5793fd6382 100644 --- a/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js +++ b/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js @@ -1,11 +1,27 @@ import React from 'react' import { get, head } from 'lodash' -import { Icon, th } from '@pubsweet/ui' -import styled, { css, withTheme } from 'styled-components' +import { connect } from 'react-redux' +import { actions } from 'pubsweet-client' +import { Icon, Button, th } from '@pubsweet/ui' import { compose, withHandlers } from 'recompose' -import AssignEditor from './AssignEditor' +import styled, { css, withTheme } from 'styled-components' +import { + withModal, + ConfirmationModal, + SuccessModal, +} from 'pubsweet-component-modal/src/components' + +import { revokeHandlingEditor, assignHandlingEditor } from '../../redux/editors' -const HandlingEditorActions = ({ project, theme, getHandlingEditor }) => { +import HEModal from './AssignHEModal' + +const HandlingEditorActions = ({ + project, + theme, + getHandlingEditor, + showConfirmModal, + showHEModal, +}) => { const handlingEditor = getHandlingEditor() return ( <Root> @@ -15,21 +31,51 @@ const HandlingEditorActions = ({ project, theme, getHandlingEditor }) => { <HEName>{get(handlingEditor, 'name')}</HEName> {!handlingEditor.hasAnswer && ( <HEActions> - <Icon color={theme.colorPrimary}>refresh-cw</Icon> - <Icon color={theme.colorPrimary}>x-circle</Icon> + <div onClick={showConfirmModal('resend')}> + <Icon color={theme.colorPrimary}>refresh-cw</Icon> + </div> + <div onClick={showConfirmModal('cancel')}> + <Icon color={theme.colorPrimary}>x-circle</Icon> + </div> </HEActions> )} </HEActions> ) : ( - <AssignEditor collectionId={project.id} /> + <AssignButton onClick={showHEModal}>Assign</AssignButton> )} </HEActions> </Root> ) } +const CardModal = ({ type, ...rest }) => { + switch (type) { + case 'confirmation': + return <ConfirmationModal {...rest} /> + case 'success': + return <SuccessModal {...rest} /> + case 'he-modal': + default: + return <HEModal {...rest} /> + } +} + +const handleError = fn => e => { + fn(get(JSON.parse(e.response), 'error') || 'Oops! Something went wrong!') +} + export default compose( + connect(null, { + revokeHandlingEditor, + assignHandlingEditor, + updateCollection: actions.updateCollection, + getCollections: actions.getCollections, + }), withTheme, + withModal({ + modalKey: 'confirmHE', + modalComponent: CardModal, + }), withHandlers({ getHandlingEditor: ({ project }) => () => { const assignedEditors = get(project, 'assignedPeople') @@ -44,6 +90,64 @@ export default compose( return null }, }), + withHandlers({ + showConfirmModal: ({ + showModal, + project, + revokeHandlingEditor, + assignHandlingEditor, + getHandlingEditor, + hideModal, + setModalError, + updateCollection, + getCollections, + }) => actionType => { + const editor = getHandlingEditor() + const resendConfig = { + title: 'Resend Invitation?', + subtitle: '', + confirmText: 'Resend', + onConfirm: () => + assignHandlingEditor(get(editor, 'email'), project.id, true).then( + () => { + hideModal() + showModal({ + type: 'success', + title: 'Invite resent', + }) + }, + handleError(setModalError), + ), + } + const revokeConfig = { + title: 'Revoke Handling Editor Assignation?', + subtitle: `Clicking 'Revoke' will allow you to invite a different person.`, + confirmText: 'Revoke invite', + onConfirm: () => + revokeHandlingEditor(get(editor, 'id'), project.id).then(() => { + updateCollection({ + id: project.id, + status: 'submitted', + }).then(() => { + getCollections() + hideModal() + showModal({ + type: 'success', + title: 'Handling Editor Assignation Revoked', + }) + }) + }, handleError(setModalError)), + } + + return () => { + const cfg = actionType === 'resend' ? resendConfig : revokeConfig + showModal({ ...cfg, type: 'confirmation' }) + } + }, + showHEModal: ({ showModal, project }) => () => { + showModal({ type: 'he-modal', collectionId: project.id, showModal }) + }, + }), )(HandlingEditorActions) // #region styled-components @@ -75,4 +179,13 @@ const HEActions = styled.div` } } ` + +const AssignButton = styled(Button)` + ${defaultText}; + align-items: center; + background-color: ${th('colorPrimary')}; + color: ${th('colorTextReverse')}; + text-align: center; + height: calc(${th('subGridUnit')}*5); +` // #endregion diff --git a/packages/components-faraday/src/components/Dashboard/utils.js b/packages/components-faraday/src/components/Dashboard/utils.js index de53b9268bf0cd284e2186f97ffdb0abdc974788..c3604010acb437e90aaeb9528b7826d8419b5586 100644 --- a/packages/components-faraday/src/components/Dashboard/utils.js +++ b/packages/components-faraday/src/components/Dashboard/utils.js @@ -74,3 +74,12 @@ export const parseVersion = version => ({ export const parseJournalIssue = (journal, metadata) => journal.issueTypes.find(t => t.value === get(metadata, 'issue')) + +export const mapStatusToLabel = status => { + switch (status) { + case 'he-invited': + return 'Handling Editor Invited' + default: + return 'Submitted' + } +} diff --git a/packages/components-faraday/src/redux/editors.js b/packages/components-faraday/src/redux/editors.js index 77fee22d73b2f328a8e718a8a028d30b74ad3955..4203a858219f6261cb37f05a39e2109c64a7f90b 100644 --- a/packages/components-faraday/src/redux/editors.js +++ b/packages/components-faraday/src/redux/editors.js @@ -25,7 +25,7 @@ export const assignHandlingEditor = ( resend, }) -export const revokeHandlingEditor = (collectionId, userId) => dispatch => +export const revokeHandlingEditor = (userId, collectionId) => dispatch => remove(`/collections/${collectionId}/users/${userId}?role=handlingEditor`) const initialState = []