diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index 728b2e28a08eb1572980a1879a27e4a6e5eb98f5..3dcdaa7a4cea7472a0d05a947f1a318fc9aee9a3 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -1,6 +1,5 @@ import { selectCurrentUser } from 'xpub-selectors' -// eslint-disable-next-line no-unused-vars -import { get, has, last, chain, some, isEmpty, flatten } from 'lodash' +import { get, has, last, chain, some, isEmpty, slice, find } from 'lodash' export const isHEToManuscript = (state, collectionId = '') => { const { id = '', isAccepted = false } = chain(state) @@ -49,6 +48,29 @@ export const canInviteReviewers = (state, collection = {}) => { return isAccepted && (userId === heId || isAdminEiC) } +const canViewContextualBoxOnOldVersionStatuses = [ + 'submitted', + 'heInvited', + 'heAssigned', +] +const canViewContextualBoxOnOldVersion = (collection, fragmentId) => { + const fragments = get(collection, 'fragments', []) + const oldVersions = slice(fragments, 0, fragments.length - 1) + const isOldVersion = !!find(oldVersions, fragment => fragment === fragmentId) + return ( + isOldVersion && + canViewContextualBoxOnOldVersionStatuses.includes( + get(collection, 'status', 'draft'), + ) + ) +} + +const canHEViewContextualBoxOnOldVersion = (collection, fragmentId) => { + const fragments = get(collection, 'fragments', []) + const oldVersions = slice(fragments, 0, fragments.length - 1) + const isOldVersion = !!find(oldVersions, fragment => fragment === fragmentId) + return isOldVersion && get(collection, 'status', 'draft') === 'heInvited' +} const cannotViewReviewersDetails = [ 'draft', 'technicalChecks', @@ -83,9 +105,11 @@ export const authorCanViewReportsDetails = ( ) => { const isAuthor = currentUserIsAuthor(state, fragmentId) return ( - authorCanViewReportsDetailsStatuses.includes( + isAuthor && + (authorCanViewReportsDetailsStatuses.includes( get(collection, 'status', 'draft'), - ) && isAuthor + ) || + canViewContextualBoxOnOldVersion(collection, fragmentId)) ) } @@ -176,8 +200,13 @@ export const canViewEditorialComments = ( state, fragmentId, ) + const isHE = currentUserIs(state, 'isHE') + const canViewEditorialCommentsOnOldVersion = isHE + ? !canHEViewContextualBoxOnOldVersion(collection, fragmentId) + : canViewContextualBoxOnOldVersion(collection, fragmentId) return ( - (canHeViewEditorialComments(state, collection) || + (canViewEditorialCommentsOnOldVersion || + canHeViewEditorialComments(state, collection) || canEICViewEditorialComments(state, collection) || canReviewerViewEditorialComments(state, collection, fragment) || canAuthorViewEditorialComments(state, collection, fragmentId)) && @@ -185,17 +214,22 @@ export const canViewEditorialComments = ( ) } -const cannotViewResponseFromAuthorStatuses = ['reviewersInvited'] export const canViewResponseFromAuthor = (state, collection, fragmentId) => { const authorResponseToRevisonRequest = getFragmentAuthorResponse( state, fragmentId, ) + const canHEViewResponseFromAuthor = + currentUserIs(state, 'isHE') && + get(collection, 'status', 'draft') === 'heInvited' + + const canReviewerViewResponsefromAuthor = + currentUserIsReviewerInPending(state, fragmentId) && + get(collection, 'status', 'draft') === 'reviewersInvited' return ( !isEmpty(authorResponseToRevisonRequest) && - !cannotViewResponseFromAuthorStatuses.includes( - get(collection, 'status', 'draft'), - ) + !canHEViewResponseFromAuthor && + !canReviewerViewResponsefromAuthor ) } @@ -326,6 +360,13 @@ export const pendingReviewerInvitation = (state, fragmentId) => ) .value() +export const currentUserIsReviewerInPending = (state, fragmentId) => { + const currentUser = selectCurrentUser(state) + const invitations = get(state, `fragments.${fragmentId}.invitations`, []) + return !!invitations.find( + i => i.userId === currentUser.id && i.role === 'reviewer' && !i.isAccepted, + ) +} export const currentUserIsReviewer = (state, fragmentId) => { const currentUser = selectCurrentUser(state) const invitations = get(state, `fragments.${fragmentId}.invitations`, []) diff --git a/packages/component-faraday-ui/src/PersonInvitation.js b/packages/component-faraday-ui/src/PersonInvitation.js index 5b1da63fab1b7fb1b129b175fce6cd0518f4b146..17ed1e3ac350499eb5c2660c697b48c4b2d94e22 100644 --- a/packages/component-faraday-ui/src/PersonInvitation.js +++ b/packages/component-faraday-ui/src/PersonInvitation.js @@ -9,6 +9,7 @@ const PersonInvitation = ({ withName, hasAnswer, isFetching, + isLatestVersion, revokeInvitation, resendInvitation, person: { name, email }, @@ -57,6 +58,29 @@ const PersonInvitation = ({ </OpenModal> </Fragment> )} + {hasAnswer && + isLatestVersion && ( + <Fragment> + <OpenModal + confirmText="Revoke" + isFetching={isFetching} + modalKey={`remove-${id}`} + onConfirm={revokeInvitation} + subtitle={`Are you sure you want to remove ${email}? This decision will erase all data from the current fragment.`} + title="Revoke invitation?" + > + {showModal => ( + <IconButton + icon="x-circle" + iconSize={2} + ml={2} + onClick={showModal} + secondary + /> + )} + </OpenModal> + </Fragment> + )} </Root> ) diff --git a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptHeader.js b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptHeader.js index 699edec06e90cfc92695a9cbec10bc2ca8e81d70..9c6f5b851a29553f89aae6ee59db2647f1ee58f6 100644 --- a/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptHeader.js +++ b/packages/component-faraday-ui/src/manuscriptDetails/ManuscriptHeader.js @@ -121,6 +121,7 @@ export default compose( revokeInvitation, pendingInvitation = {}, handlingEditors = [], + isLatestVersion, currentUser: { permissions: { canAssignHE }, id: currentUserId, @@ -128,24 +129,36 @@ export default compose( editorInChief, }, collection: { handlingEditor }, + collection, currentUser, }) => () => { if (pendingInvitation.userId === currentUserId) { return <Text ml={1}>Invited</Text> } - if (pendingInvitation.userId && (admin || editorInChief)) { + if ( + (get(pendingInvitation, 'userId', false) || + get(heInvitation, 'userId', false)) && + (admin || editorInChief) + ) { const person = chain(handlingEditors) - .filter(he => he.id === pendingInvitation.userId) + .filter(he => he.id === get(heInvitation, 'userId', false)) .map(he => ({ ...he, name: `${he.firstName} ${he.lastName}` })) .first() .value() + let invitedPerson = {} + if (get(pendingInvitation, 'userId', false)) { + invitedPerson = pendingInvitation + } else if (get(heInvitation, 'userId', false)) { + invitedPerson = heInvitation + } return ( <PersonInvitation isFetching={isFetching} + isLatestVersion={isLatestVersion} ml={1} withName - {...pendingInvitation} + {...invitedPerson} onResend={resendInvitation} onRevoke={revokeInvitation} person={person} diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js index 0db955323c5e15fe008bf2ef90152788d078df46..b0789f05aa93601db62053ce8f5f18382b944973 100644 --- a/packages/component-helper-service/src/services/Collection.js +++ b/packages/component-helper-service/src/services/Collection.js @@ -147,17 +147,19 @@ class Collection { [], ) - const lastRecommendationByHE = findLast( + const lastEditorRecommendation = findLast( previousVersionRecommendations, recommendation => - recommendation.userId === this.collection.handlingEditor.id && recommendation.recommendationType === 'editorRecommendation', ) - if (lastRecommendationByHE.recommendation === 'minor') { + + if (lastEditorRecommendation.recommendation === 'minor') { return this.hasAtLeastOneReviewReport(fragments) - } else if (lastRecommendationByHE.recommendation === 'major') { + } else if (lastEditorRecommendation.recommendation === 'major') { return fragmentHelper.hasReviewReport() } + + return false } async getAllFragments({ FragmentModel }) { diff --git a/packages/component-invite/src/CollectionsInvitations.js b/packages/component-invite/src/CollectionsInvitations.js index 2f80dd1b70e3089a7ab7f9068a4e12dbb84985e5..0b72a54da398154f0b1e82b5f78601d57cf86064 100644 --- a/packages/component-invite/src/CollectionsInvitations.js +++ b/packages/component-invite/src/CollectionsInvitations.js @@ -39,7 +39,7 @@ const CollectionsInvitations = app => { require(`${routePath}/post`)(app.locals.models), ) /** - * @api {delete} /api/collections/:collectionId/invitations/:invitationId Delete invitation + * @api {delete} /api/collections/:collectionId/invitations/:invitationId Delete invitation (or revoke HE if invitation is accepted) * @apiGroup CollectionsInvitations * @apiParam {collectionId} collectionId Collection id * @apiParam {invitationId} invitationId Invitation id diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js index 6b3b6764bc654b1f05ea8b4402645649abf7243a..75bc11393f4356d744d9f38f7c4e997db0f129ab 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/delete.js +++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js @@ -1,8 +1,17 @@ +const config = require('config') + const { Team, services, authsome: authsomeHelper, } = require('pubsweet-component-helper-service') +const { + deleteFilesS3, +} = require('pubsweet-component-mts-package/src/PackageManager') + +const { last, get, chain, difference } = require('lodash') + +const s3Config = get(config, 'pubsweet-component-aws-s3', {}) const notifications = require('./emails/notifications') @@ -56,6 +65,46 @@ module.exports = models => async (req, res) => { user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) await user.save() + if (invitation.hasAnswer && invitation.isAccepted) { + const FragmentModel = models.Fragment + const fragment = await FragmentModel.find( + last(get(collection, 'fragments', [])), + ) + + const fileKeys = [] + fragment.recommendations && + fragment.recommendations.forEach(recommendation => { + recommendation.comments.forEach(comment => { + comment.files && + comment.files.forEach(file => { + fileKeys.push(file.id) + }) + }) + }) + + const revision = get(fragment, 'revision', false) + if (revision) { + const fragmentFilesIds = chain(get(fragment, 'files', [])) + .flatMap(item => item) + .map(item => item.id) + .value() + const revisionFilesIds = chain(get(fragment, 'revision.files', [])) + .flatMap(item => item) + .map(item => item.id) + .value() + const revisionFileIds = difference(revisionFilesIds, fragmentFilesIds) + fileKeys.concat(revisionFileIds) + } + if (fileKeys.length > 1) { + await deleteFilesS3({ fileKeys, s3Config }) + } + + fragment.invitations = [] + fragment.recommendations = [] + fragment.revision && delete fragment.revision + fragment.save() + } + notifications.sendInvitedHEEmail({ models, collection, diff --git a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js index c6e6089b02c4946f6a624911036cf2736a905d77..184f8818018ca0bab8fb24db9a5e844cc1273ed3 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/delete.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/delete.test.js @@ -9,6 +9,9 @@ const { Model, fixtures } = fixturesService jest.mock('@pubsweet/component-send-email', () => ({ send: jest.fn(), })) +jest.mock('pubsweet-component-mts-package/src/PackageManager', () => ({ + deleteFilesS3: jest.fn(), +})) const path = '../routes/collectionsInvitations/delete' const route = { @@ -86,4 +89,19 @@ describe('Delete Collections Invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Unauthorized.') }) + it('should return success when the EiC revokes a HE', async () => { + const { editorInChief } = testFixtures.users + const { collection } = testFixtures.collections + const res = await requests.sendRequest({ + userId: editorInChief.id, + route, + models, + path, + params: { + collectionId: collection.id, + invitationId: collection.invitations[1].id, + }, + }) + expect(res.statusCode).toBe(200) + }) }) diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index b8492c8be9ffd0e73b77264ce5a0fd00be719cee..0d1abdeb62cf16434d58430900d4b5a0f0bb5d2d 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -164,17 +164,18 @@ const ManuscriptLayout = ({ /> )} - {get(currentUser, 'isInvitedHE', false) && ( - <ResponseToInvitation - commentsOn="decline" - expanded={heResponseExpanded} - formValues={formValues.responseToInvitation} - label="Do you agree to be the handling editor for this manuscript?" - onResponse={inviteHandlingEditor.onHEResponse} - title="Respond to Editorial Invitation" - toggle={toggleHEResponse} - /> - )} + {isLatestVersion && + get(currentUser, 'isInvitedHE', false) && ( + <ResponseToInvitation + commentsOn="decline" + expanded={heResponseExpanded} + formValues={formValues.responseToInvitation} + label="Do you agree to be the handling editor for this manuscript?" + onResponse={inviteHandlingEditor.onHEResponse} + title="Respond to Editorial Invitation" + toggle={toggleHEResponse} + /> + )} {get(currentUser, 'isInvitedToReview', false) && ( <ResponseToInvitation @@ -186,14 +187,16 @@ const ManuscriptLayout = ({ /> )} - <ManuscriptAssignHE - assignHE={inviteHandlingEditor.assignHE} - currentUser={currentUser} - expanded={heExpanded} - handlingEditors={handlingEditors} - isFetching={isFetchingData.editorsFetching} - toggle={toggleAssignHE} - /> + {isLatestVersion && ( + <ManuscriptAssignHE + assignHE={inviteHandlingEditor.assignHE} + currentUser={currentUser} + expanded={heExpanded} + handlingEditors={handlingEditors} + isFetching={isFetchingData.editorsFetching} + toggle={toggleAssignHE} + /> + )} {get(currentUser, 'permissions.canViewReviewersDetails', false) && ( <ReviewerDetails diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 89b5dadefcc611d56684ad1b442b326f2f3870ab..49431eac63aad495e794a5c5a5b6e749323191df 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -177,7 +177,11 @@ export default compose( collection, statuses: get(journal, 'statuses', {}), }), - canAssignHE: canAssignHE(state, collection), + canAssignHE: canAssignHE( + state, + collection, + isLatestVersion(collection, fragment), + ), canViewReports: canViewReports(state, match.params.project), canViewEditorialComments: canViewEditorialComments( state, diff --git a/packages/component-manuscript/src/redux/editors.js b/packages/component-manuscript/src/redux/editors.js index e28c0dd29ff392776fa2092639ec8fceda19026a..2ff500a7c82c6d94db4b74faaa837b961b6c1b04 100644 --- a/packages/component-manuscript/src/redux/editors.js +++ b/packages/component-manuscript/src/redux/editors.js @@ -24,13 +24,14 @@ export const selectHandlingEditors = state => .value() const canAssignHEStatuses = ['submitted'] -export const canAssignHE = (state, collection = {}) => { +export const canAssignHE = (state, collection = {}, isLatestVersion) => { const isEIC = currentUserIs(state, 'adminEiC') const hasHE = get(collection, 'handlingEditor', false) return ( isEIC && !hasHE && + isLatestVersion && canAssignHEStatuses.includes(get(collection, 'status', 'draft')) ) }