diff --git a/packages/component-faraday-ui/src/ReviewerReport.js b/packages/component-faraday-ui/src/ReviewerReport.js index 28e92f382248941e3607853f0915723a1516966a..fc2c942c5bb4fdee93bfcbd64cc2bb05d768d3f2 100644 --- a/packages/component-faraday-ui/src/ReviewerReport.js +++ b/packages/component-faraday-ui/src/ReviewerReport.js @@ -15,7 +15,7 @@ const ReviewerReport = ({ publicReport, privateReport, reviewerName, - reviewerIndex, + reviewerNumber, recommendation, showOwner = false, report: { submittedOn }, @@ -31,7 +31,7 @@ const ReviewerReport = ({ <Fragment> {showOwner && <Text>{reviewerName}</Text>} <Text customId ml={1} mr={1}> - {`Reviewer ${reviewerIndex}`} + {`Reviewer ${reviewerNumber}`} </Text> </Fragment> <DateParser timestamp={submittedOn}> diff --git a/packages/component-faraday-ui/src/ReviewerReportAuthor.js b/packages/component-faraday-ui/src/ReviewerReportAuthor.js index 4df5745b8ce86a109bad547466f3fe5ff57f94de..ded36d3ae2e8128002c2bbde7a0292d172d44d66 100644 --- a/packages/component-faraday-ui/src/ReviewerReportAuthor.js +++ b/packages/component-faraday-ui/src/ReviewerReportAuthor.js @@ -21,7 +21,7 @@ const ReviewerReportAuthor = ({ downloadFile, publicReport, reviewerName, - reviewerIndex, + reviewerNumber, recommendation, showOwner = false, report: { submittedOn }, @@ -38,7 +38,7 @@ const ReviewerReportAuthor = ({ </Row> )} <Text customId ml={1} mr={1} whiteSpace="nowrap"> - {`Reviewer ${reviewerIndex}`} + {`Reviewer ${reviewerNumber}`} </Text> <DateParser timestamp={submittedOn}> {date => <Text>{date}</Text>} @@ -78,7 +78,7 @@ export default compose( 'reviewer.lastName', '', )}`, - reviewerIndex: get(report, 'reviewerIndex', ''), + reviewerNumber: get(report, 'reviewerNumber', ''), })), )(ReviewerReportAuthor) diff --git a/packages/component-faraday-ui/src/ReviewerReportAuthor.md b/packages/component-faraday-ui/src/ReviewerReportAuthor.md index 48fdf8f4faefd2db6af22906b08d22899c94edaa..0ee98012afcf5a17ddccc438eecce6524d3f195b 100644 --- a/packages/component-faraday-ui/src/ReviewerReportAuthor.md +++ b/packages/component-faraday-ui/src/ReviewerReportAuthor.md @@ -29,7 +29,7 @@ const report = { submittedOn: 1538053600624, recommendation: 'publish', recommendationType: 'review', - reviewerIndex: 1 + reviewerNumber: 1 } const journal = { diff --git a/packages/component-faraday-ui/src/ReviewersTable.js b/packages/component-faraday-ui/src/ReviewersTable.js index 9f9e2aaa75309afd25084e94a65cbcf65e2c646b..e2197d20c406eeff827b6b4051f48062d7e7493e 100644 --- a/packages/component-faraday-ui/src/ReviewersTable.js +++ b/packages/component-faraday-ui/src/ReviewersTable.js @@ -41,9 +41,9 @@ const ReviewersTable = ({ invitation, 'person.lastName', )}`}</Text> - {invitation.isAccepted && ( + {invitation.reviewerNumber && ( <Text customId ml={1}> - {renderAcceptedLabel(index)} + Reviewer {invitation.reviewerNumber} </Text> )} </td> @@ -102,12 +102,7 @@ export default compose( withProps(({ invitations = [] }) => ({ invitations: orderBy(invitations, orderInvitations), })), - withProps(({ invitations = [] }) => ({ - firstAccepted: invitations.findIndex(i => i.hasAnswer && i.isAccepted), - })), withHandlers({ - renderAcceptedLabel: ({ firstAccepted, invitations }) => index => - `Reviewer ${index - firstAccepted + 1}`, getInvitationStatus: () => ({ hasAnswer, isAccepted }) => { if (!hasAnswer) return 'PENDING' if (isAccepted) return 'ACCEPTED' diff --git a/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js b/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js index 5518b3ebfa9c3803816c871f4f8b32b717c468d3..696e8a4e405ca71b5e6844cfedf81994863060a2 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js +++ b/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.js @@ -1,7 +1,14 @@ import React from 'react' import { withProps, compose } from 'recompose' +import { get } from 'lodash' -import { ContextualBox, ReviewerReportAuthor, Row, Text } from '../' +import { + ContextualBox, + ReviewerReportAuthor, + Row, + Text, + indexReviewers, +} from '../' const SubmittedReportsNumberForAuthorReviews = ({ reports }) => ( <Row fitContent justify="flex-end"> @@ -16,12 +23,13 @@ const SubmittedReportsNumberForAuthorReviews = ({ reports }) => ( ) const AuthorReviews = ({ - invitations, + token, journal, reports, fragment, - token, + invitations, getSignedUrl, + reviewerReports, }) => reports.length > 0 && ( <ContextualBox @@ -43,4 +51,24 @@ const AuthorReviews = ({ </ContextualBox> ) -export default compose(withProps())(AuthorReviews) +export default compose( + withProps( + ({ + invitations = [], + publonReviewers = [], + reviewerReports = [], + currentUser, + }) => ({ + token: get(currentUser, 'token', ''), + publonReviewers, + invitations: invitations.map(i => ({ + ...i, + review: reviewerReports.find(r => r.userId === i.userId), + })), + reports: indexReviewers( + reviewerReports.filter(r => r.submittedOn), + invitations, + ), + }), + ), +)(AuthorReviews) diff --git a/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.md b/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.md index aa15139b436a8238511b82742f9202d74516f9ce..6bb32f81d1495f66987b964ce3451a619b57d2ea 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.md +++ b/packages/component-faraday-ui/src/contextualBoxes/AuthorReviews.md @@ -28,7 +28,7 @@ const reports = [ submittedOn: 1539339580826, recommendation: 'minor', recommendationType: 'review', - reviewerIndex: 1, + reviewerNumber: 1, }, { id: '21258b47-aba5-4597-926e-765458c4fda2', @@ -45,7 +45,7 @@ const reports = [ submittedOn: 1539689169611, recommendation: 'publish', recommendationType: 'review', - reviewerIndex: 2, + reviewerNumber: 2, }, ] diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js index d8a41b821abe0639bbb6f9cfbe6ffe17b9f75189..987aa4a775f7eed363cfcfe0793c5e4baeb1468c 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js @@ -13,6 +13,7 @@ import { ContextualBox, ReviewersTable, PublonsTable, + indexReviewers, ReviewerReport, InviteReviewers, ReviewerBreakdown, @@ -118,14 +119,14 @@ const ReviewerDetails = ({ {reports.length === 0 && ( <Text align="center">No reports submitted yet.</Text> )} - {reports.map((report, index) => ( + {reports.map(report => ( <ReviewerReport journal={journal} key={report.id} onDownload={downloadFile} onPreview={previewFile} report={report} - reviewerIndex={index + 1} + reviewerNumber={report.reviewerNumber} showOwner /> ))} @@ -154,7 +155,10 @@ export default compose( ...i, review: reviewerReports.find(r => r.userId === i.userId), })), - reports: reviewerReports.filter(r => r.submittedOn), + reports: indexReviewers( + reviewerReports.filter(r => r.submittedOn), + invitations, + ), }), ), withProps(({ currentUser }) => ({ diff --git a/packages/component-faraday-ui/src/helpers/utils.js b/packages/component-faraday-ui/src/helpers/utils.js index 898fd3ebc62282763a1331fd359f9d8da85a732c..8b798e34e7b73272c922ab6201019b5b4b221fb9 100644 --- a/packages/component-faraday-ui/src/helpers/utils.js +++ b/packages/component-faraday-ui/src/helpers/utils.js @@ -1,4 +1,4 @@ -import { get, chain } from 'lodash' +import { get, chain, find } from 'lodash' export const handleError = fn => e => { fn(get(JSON.parse(e.response), 'error', 'Oops! Something went wrong!')) @@ -10,3 +10,14 @@ export const getReportComments = ({ report, isPublic = false }) => .find(c => c.public === isPublic) .get('content') .value() + +export const indexReviewers = (reports = [], invitations = []) => { + reports.forEach(report => { + report.reviewerNumber = get( + find(invitations, ['userId', report.userId]), + 'reviewerNumber', + 0, + ) + }) + return reports +} diff --git a/packages/component-fixture-manager/src/fixtures/collectionIDs.js b/packages/component-fixture-manager/src/fixtures/collectionIDs.js index e66bb71a232758a7de0746033ba2493745157250..a964c2c498c452599e68dd3457a24eac4c0a3b49 100644 --- a/packages/component-fixture-manager/src/fixtures/collectionIDs.js +++ b/packages/component-fixture-manager/src/fixtures/collectionIDs.js @@ -7,5 +7,6 @@ module.exports = { collectionReviewCompletedID: chance.guid(), collectionNoInvitesID: chance.guid(), twoVersionsCollectionId: chance.guid(), + oneReviewedFragmentCollectionID: chance.guid(), noEditorRecomedationCollectionID: chance.guid(), } diff --git a/packages/component-fixture-manager/src/fixtures/collections.js b/packages/component-fixture-manager/src/fixtures/collections.js index 7d1df81e4f6379f9bb4d21bc243a2ed97a9e9e2d..0fbff658ca84fc9266a31674461679e769a1ff13 100644 --- a/packages/component-fixture-manager/src/fixtures/collections.js +++ b/packages/component-fixture-manager/src/fixtures/collections.js @@ -17,6 +17,7 @@ const { collectionReviewCompletedID, collectionNoInvitesID, twoVersionsCollectionId, + oneReviewedFragmentCollectionID, noEditorRecomedationCollectionID, } = require('./collectionIDs') @@ -30,6 +31,7 @@ const collections = { fragments: [fragment.id], owners: [user.id], save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => [fragment]), invitations: [ { id: chance.guid(), @@ -73,6 +75,7 @@ const collections = { fragments: [fragment.id], owners: [user.id], save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => [fragment]), invitations: [ { id: chance.guid(), @@ -115,6 +118,7 @@ const collections = { fragments: [fragment1.id, noInvitesFragment.id], owners: [user.id], save: jest.fn(() => collections.collection2), + getFragments: jest.fn(() => [fragment1, noInvitesFragment]), invitations: [ { id: chance.guid(), @@ -159,6 +163,7 @@ const collections = { created: chance.timestamp(), customId: '0000001', fragments: [reviewCompletedFragment.id], + getFragments: jest.fn(() => [reviewCompletedFragment]), invitations: [ { id: chance.guid(), @@ -189,6 +194,7 @@ const collections = { fragments: [fragment.id, reviewCompletedFragment.id], owners: [user.id], save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => [fragment, reviewCompletedFragment]), invitations: [ { id: chance.guid(), @@ -219,6 +225,7 @@ const collections = { fragments: [], owners: [user.id], save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => []), customId: chance.natural({ min: 999999, max: 9999999 }), }, noEditorRecomedationCollection: { @@ -228,6 +235,7 @@ const collections = { fragments: [noEditorRecomedationFragment.id], owners: [user.id], save: jest.fn(() => collections.noEditorRecomedationCollection), + getFragments: jest.fn(() => [noEditorRecomedationFragment]), invitations: [ { id: chance.guid(), @@ -263,6 +271,37 @@ const collections = { }, status: 'reviewCompleted', }, + oneReviewedFragmentCollection: { + id: oneReviewedFragmentCollectionID, + title: chance.sentence(), + type: 'collection', + fragments: [reviewCompletedFragment.id, noInvitesFragment.id], + owners: [user.id], + save: jest.fn(() => collections.collection), + getFragments: jest.fn(() => [reviewCompletedFragment, noInvitesFragment]), + invitations: [ + { + id: chance.guid(), + role: 'handlingEditor', + hasAnswer: true, + isAccepted: false, + userId: handlingEditor.id, + invitedOn: chance.timestamp(), + respondedOn: null, + }, + ], + handlingEditor: { + id: handlingEditor.id, + hasAnswer: false, + isAccepted: false, + email: handlingEditor.email, + invitedOn: chance.timestamp(), + respondedOn: null, + name: `${handlingEditor.firstName} ${handlingEditor.lastName}`, + }, + status: 'revisionRequested', + customId: chance.natural({ min: 999999, max: 9999999 }), + }, } module.exports = collections diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js index 15e4cb3fa981f4316d1d686a6dc2414d783ed89b..337c0d943c7e6fd87e47fd3e31ff3458c0fb567d 100644 --- a/packages/component-fixture-manager/src/fixtures/fragments.js +++ b/packages/component-fixture-manager/src/fixtures/fragments.js @@ -340,6 +340,7 @@ const fragments = { invitedOn: chance.timestamp(), isAccepted: true, respondedOn: chance.timestamp(), + reviewerNumber: 2, }, { id: chance.guid(), diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js index 6e05d9a584f61eddc5a63a7ad309ac81bea1edb6..0db955323c5e15fe008bf2ef90152788d078df46 100644 --- a/packages/component-helper-service/src/services/Collection.js +++ b/packages/component-helper-service/src/services/Collection.js @@ -1,4 +1,4 @@ -const { findLast, get } = require('lodash') +const { findLast, isEmpty, maxBy, get, flatMap } = require('lodash') const Fragment = require('./Fragment') @@ -109,6 +109,27 @@ class Collection { return lastName || firstName } + async getReviewerNumber({ userId }) { + const allCollectionFragments = await this.collection.getFragments() + const allCollectionInvitations = flatMap( + allCollectionFragments, + fragment => fragment.invitations, + ) + const allNumberedInvitationsForUser = allCollectionInvitations + .filter(invite => invite.userId === userId) + .filter(invite => invite.reviewerNumber) + + if (isEmpty(allNumberedInvitationsForUser)) { + const maxReviewerNumber = get( + maxBy(allCollectionInvitations, 'reviewerNumber'), + 'reviewerNumber', + 0, + ) + return maxReviewerNumber + 1 + } + return allNumberedInvitationsForUser[0].reviewerNumber + } + // eslint-disable-next-line class-methods-use-this hasAtLeastOneReviewReport(fragments) { return fragments.some(fragment => diff --git a/packages/component-helper-service/src/tests/collection.test.js b/packages/component-helper-service/src/tests/collection.test.js index 41c9f0ed6481079f790df50fc83584af62f2d1c4..167153af8c03fc7149c36a9e8c149de351c0f7db 100644 --- a/packages/component-helper-service/src/tests/collection.test.js +++ b/packages/component-helper-service/src/tests/collection.test.js @@ -10,11 +10,65 @@ const { Collection, Fragment } = require('../Helper') describe('Collection helper', () => { let testFixtures = {} let models + beforeEach(() => { testFixtures = cloneDeep(fixtures) models = Model.build(testFixtures) }) + describe('getReviewerNumber', () => { + it('should assign reviewer number 1 on invitation if no other reviewer numbers exist', async () => { + const { collection } = testFixtures.collections + const { reviewer } = testFixtures.users + const collectionHelper = new Collection({ collection }) + + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId: reviewer.id, + }) + + expect(reviewerNumber).toBe(1) + }) + it('should assign next reviewer number on invitation if another reviewer numbers exist', async () => { + const { collectionReviewCompleted } = testFixtures.collections + const { reviewer } = testFixtures.users + const collectionHelper = new Collection({ + collection: collectionReviewCompleted, + }) + + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId: reviewer.id, + }) + + expect(reviewerNumber).toBe(3) + }) + it('should keep reviewer number across fragment versions', async () => { + const { oneReviewedFragmentCollection } = testFixtures.collections + const { answerReviewer } = testFixtures.users + const collectionHelper = new Collection({ + collection: oneReviewedFragmentCollection, + }) + + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId: answerReviewer.id, + }) + + expect(reviewerNumber).toBe(2) + }) + it('should assign next reviewer number across fragment versions', async () => { + const { oneReviewedFragmentCollection } = testFixtures.collections + const { reviewer } = testFixtures.users + const collectionHelper = new Collection({ + collection: oneReviewedFragmentCollection, + }) + + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId: reviewer.id, + }) + + expect(reviewerNumber).toBe(3) + }) + }) + describe('hasAtLeastOneReviewReport', () => { it('should return true if collection has at least one report from reviewers.', async () => { const { collection } = testFixtures.collections diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js index 7846722ed2f83db255a59ff80f95c142ccf2d3ec..7cd5b2ae49984d23fff26a53ae1bfb38a19f8244 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js @@ -1,3 +1,4 @@ +const { find } = require('lodash') const { services, authsome: authsomeHelper, @@ -8,6 +9,7 @@ const Notification = require('../../notifications/notification') module.exports = models => async (req, res) => { const { collectionId, fragmentId, recommendationId } = req.params + const userId = req.user let collection, fragment try { collection = await models.Collection.find(collectionId) @@ -25,7 +27,7 @@ module.exports = models => async (req, res) => { if (!recommendation) return res.status(404).json({ error: 'Recommendation not found.' }) - if (recommendation.userId !== req.user) + if (recommendation.userId !== userId) return res.status(403).json({ error: 'Unauthorized.', }) @@ -35,14 +37,14 @@ module.exports = models => async (req, res) => { fragment, path: req.route.path, } - const canPatch = await authsome.can(req.user, 'PATCH', target) + const canPatch = await authsome.can(userId, 'PATCH', target) if (!canPatch) return res.status(403).json({ error: 'Unauthorized.', }) const UserModel = models.User - const reviewer = await UserModel.find(req.user) + const reviewer = await UserModel.find(userId) Object.assign(recommendation, req.body) recommendation.updatedOn = Date.now() @@ -62,6 +64,16 @@ module.exports = models => async (req, res) => { const collectionHelper = new Collection({ collection }) collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) } + + const collectionHelper = new Collection({ collection }) + const reviewerNumber = await collectionHelper.getReviewerNumber({ + userId, + }) + + find(fragment.invitations, [ + 'userId', + userId, + ]).reviewerNumber = reviewerNumber } fragment.save() diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index f292701d615da22603c9714813840a820bcf6a0b..d10d53bf2a7f7ff1d3c05554280aec71b8bb22bf 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -122,8 +122,9 @@ const ManuscriptLayout = ({ <AuthorReviews currentUser={currentUser} getSignedUrl={getSignedUrl} + invitations={invitationsWithReviewers} journal={journal} - reports={reviewerReports} + reviewerReports={reviewerReports} token={get(currentUser, 'token')} /> )} @@ -136,9 +137,10 @@ const ManuscriptLayout = ({ <ReviewerReports currentUser={currentUser} getSignedUrl={getSignedUrl} + invitations={invitationsWithReviewers} isLatestVersion={isLatestVersion} journal={journal} - reports={reviewerRecommendations} + reviewerReports={reviewerRecommendations} token={get(currentUser, 'token')} /> )} diff --git a/packages/component-manuscript/src/components/ReviewerReports.js b/packages/component-manuscript/src/components/ReviewerReports.js index 1f82bbc34eb5aee58124733cac320676efada4e2..659d3f2330753045f4cb1100a190e93d5b6307d9 100644 --- a/packages/component-manuscript/src/components/ReviewerReports.js +++ b/packages/component-manuscript/src/components/ReviewerReports.js @@ -1,5 +1,6 @@ import React from 'react' -import { compose } from 'recompose' +import { compose, withProps } from 'recompose' +import { get } from 'lodash' import { ReviewerReport, ContextualBox, @@ -7,6 +8,7 @@ import { withFileDownload, Text, Row, + indexReviewers, } from 'pubsweet-component-faraday-ui' const SubmittedReports = ({ reports }) => ( @@ -28,6 +30,9 @@ const ReviewerReports = ({ downloadFile, isLatestVersion, currentUser, + token, + invitations, + reviwerReports, }) => ( <ContextualBox label={isLatestVersion ? 'Your Report' : 'Reviewer Reports'} @@ -35,7 +40,7 @@ const ReviewerReports = ({ rightChildren={<SubmittedReports reports={reports.length} />} startExpanded > - {reports.map((report, index) => ( + {reports.map(report => ( <ReviewerReport currentUser={currentUser} journal={journal} @@ -43,11 +48,33 @@ const ReviewerReports = ({ onDownload={downloadFile} onPreview={previewFile} report={report} - reviewerIndex={index + 1} + reviewerNumber={report.reviewerNumber} showOwner={report.userId === currentUser.id} /> ))} </ContextualBox> ) -export default compose(withFileDownload, withFilePreview)(ReviewerReports) +export default compose( + withFileDownload, + withFilePreview, + withProps( + ({ + invitations = [], + publonReviewers = [], + reviewerReports = [], + currentUser, + }) => ({ + token: get(currentUser, 'token', ''), + publonReviewers, + invitations: invitations.map(i => ({ + ...i, + review: reviewerReports.find(r => r.userId === i.userId), + })), + reports: indexReviewers( + reviewerReports.filter(r => r.submittedOn), + invitations, + ), + }), + ), +)(ReviewerReports)