diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index cf7ada7241bb4906b3bacca806947a5ce986768b..af395e7147c405501a6457a8f46ba07de67f186d 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -40,16 +40,18 @@ export const currentUserIs = ({ currentUser: { user } }, role) => { } } -export const canInviteReviewers = ({ currentUser: { user } }, project) => { - const status = get(project, 'status') - if (!status || status === 'rejected' || status === 'published') return false +const cannotInviteReviewersStatuses = ['draft', 'rejected', 'published'] +export const canInviteReviewers = (state, collection) => { + if ( + cannotInviteReviewersStatuses.includes(get(collection, 'status', 'draft')) + ) + return false - const handlingEditor = get(project, 'handlingEditor') - const isAdmin = get(user, 'admin') - const isEic = get(user, 'editorInChief') - const isAccepted = get(handlingEditor, 'isAccepted') - const heId = get(handlingEditor, 'id') - return isAccepted && (user.id === heId || isAdmin || isEic) + const user = selectCurrentUser(state) + const isStaff = currentUserIs(state, 'adminEiC') + const { isAccepted, id: heId } = get(collection, 'handlingEditor', {}) + + return isAccepted && (user.id === heId || isStaff) } export const getUserToken = ({ currentUser }) => @@ -195,3 +197,15 @@ export const newestFirstParseDashboard = (state, items) => handlingEditor: parseInvitedHE(item.handlingEditor, state, item.id), })) .value() + +export const getInvitationsWithReviewersForFragment = (state, fragmentId) => + chain(state) + .get(`fragments.${fragmentId}.invitations`, []) + .filter(invitation => invitation.role === 'reviewer') + .map(invitation => ({ + ...invitation, + person: get(state, 'users.users', []).find( + reviewer => reviewer.id === invitation.userId, + ), + })) + .value() diff --git a/packages/component-faraday-ui/src/InviteReviewers.js b/packages/component-faraday-ui/src/InviteReviewers.js index 4be1711af62034edc507eb0c33adbbcfa01a3b6c..83895425aa89080f33be894d4db87df0cb8d6ef9 100644 --- a/packages/component-faraday-ui/src/InviteReviewers.js +++ b/packages/component-faraday-ui/src/InviteReviewers.js @@ -100,6 +100,7 @@ export default compose( // #region styles const Root = styled.div` + background-color: #eeeeee; padding: calc(${th('gridUnit')} * 2); ` // #endregion diff --git a/packages/component-faraday-ui/src/PersonInvitation.js b/packages/component-faraday-ui/src/PersonInvitation.js index 89676c26a54b867889d5f10c65ca33741fcc62ea..5b1da63fab1b7fb1b129b175fce6cd0518f4b146 100644 --- a/packages/component-faraday-ui/src/PersonInvitation.js +++ b/packages/component-faraday-ui/src/PersonInvitation.js @@ -2,15 +2,16 @@ import React, { Fragment } from 'react' import styled from 'styled-components' import { compose, withHandlers, defaultProps, setDisplayName } from 'recompose' -import { Text, OpenModal, IconButton, marginHelper } from './' +import { Text, OpenModal, IconButton, marginHelper, withFetching } from './' const PersonInvitation = ({ + id, withName, hasAnswer, isFetching, - person: { name }, revokeInvitation, resendInvitation, + person: { name, email }, ...rest }) => ( <Root {...rest}> @@ -19,9 +20,12 @@ const PersonInvitation = ({ name !== 'Unassigned' && ( <Fragment> <OpenModal + confirmText="Resend" isFetching={isFetching} + modalKey={`resend-${id}`} onConfirm={resendInvitation} - title="Are you sure you want to resend the invitation?" + subtitle={email} + title="Resend the invitation?" > {showModal => ( <IconButton @@ -34,11 +38,12 @@ const PersonInvitation = ({ )} </OpenModal> <OpenModal - confirmText="Remove invite" + confirmText="Revoke" isFetching={isFetching} + modalKey={`revoke-${id}`} onConfirm={revokeInvitation} - subtitle="Clicking ‘Remove’ will allow you to invite a different Handling Editor" - title="Remove invitation to Handling Editor?" + subtitle={email} + title="Revoke invitation?" > {showModal => ( <IconButton @@ -63,12 +68,19 @@ export default compose( onRevoke: id => {}, onResend: id => {}, }), + withFetching, withHandlers({ - revokeInvitation: ({ id, onRevoke }) => props => { - typeof onRevoke === 'function' && onRevoke(id, props) + revokeInvitation: ({ id, onRevoke, setFetching }) => props => { + typeof onRevoke === 'function' && onRevoke(id, { ...props, setFetching }) }, - resendInvitation: ({ id, onResend, person: { email } }) => props => { - typeof onResend === 'function' && onResend(email, props) + resendInvitation: ({ + id, + onResend, + setFetching, + person: { email }, + }) => props => { + typeof onResend === 'function' && + onResend(email, { ...props, setFetching }) }, }), setDisplayName('PersonInvitation'), diff --git a/packages/component-faraday-ui/src/ReviewerBreakdown.js b/packages/component-faraday-ui/src/ReviewerBreakdown.js index 7c181f90968c9751a32cc48458241fda72bbd246..40c6ff30e75e5d9a668aae9552f1b0a775cbb38a 100644 --- a/packages/component-faraday-ui/src/ReviewerBreakdown.js +++ b/packages/component-faraday-ui/src/ReviewerBreakdown.js @@ -57,7 +57,7 @@ export default compose( <Text mr={1 / 2}> submitted</Text> </Row> ) : ( - <Text> {`${reviewerInvitations.length} invited`}</Text> + <Text mr={1}>{`${reviewerInvitations.length} invited`}</Text> ) }, }), diff --git a/packages/component-faraday-ui/src/ReviewersTable.js b/packages/component-faraday-ui/src/ReviewersTable.js index 0bc9fbeb8a05fc60c7320ff0ef6e95390f82200c..7e597ccc8bd6727508ffc740b4c6ece34768909b 100644 --- a/packages/component-faraday-ui/src/ReviewersTable.js +++ b/packages/component-faraday-ui/src/ReviewersTable.js @@ -1,167 +1,88 @@ -import React from 'react' +import React, { Fragment } from 'react' +import { get } from 'lodash' import styled from 'styled-components' +import { shouldUpdate } from 'recompose' import { th } from '@pubsweet/ui-toolkit' import { DateParser } from '@pubsweet/ui' import { Label, PersonInvitation, Text } from '../' -const invitation = { - id: 'b4305ab6-84e6-48a3-9eb9-fbe0ec80c694', - role: 'handlingEditor', - type: 'invitation', - reason: 'because', - userId: 'cb7e3e26-6a09-4b79-a6ff-4d1235ee2381', - hasAnswer: false, - invitedOn: 713919119, - isAccepted: false, - respondedOn: 1533714034932, - person: { - id: 'cb7e3e26-6a09-4b79-a6ff-4d1235ee2381', - name: 'Toto Schilacci', - }, -} +const ReviewersTable = ({ + invitations, + onResendReviewerInvite, + onRevokeReviewerInvite, +}) => + invitations.length > 0 && ( + <Table> + <thead> + <tr> + <th> + <Label>Full Name</Label> + </th> + <th> + <Label>Invited on</Label> + </th> + <th> + <Label>Responded on</Label> + </th> + <th> + <Label>Submitted on</Label> + </th> + <th> </th> + </tr> + </thead> + <tbody> + {invitations.map((invitation, index) => ( + <TableRow key={invitation.id}> + <td> + <Text>{`${get(invitation, 'person.firstName', '')} ${get( + invitation, + 'person.lastName', + )}`}</Text> + {invitation.isAccepted && ( + <Text customId ml={1}>{`Reviewer ${index + 1}`}</Text> + )} + </td> + <td> + <DateParser timestamp={invitation.invitedOn}> + {timestamp => <Text>{timestamp}</Text>} + </DateParser> + </td> + <td> + {invitation.respondedOn && ( + <Fragment> + <DateParser timestamp={invitation.respondedOn}> + {timestamp => <Text>{timestamp}</Text>} + </DateParser> + <Text ml={1} secondary> + ACCEPTED + </Text> + </Fragment> + )} + </td> + <td> + {invitation.respondedOn && ( + <DateParser timestamp={invitation.respondedOn}> + {timestamp => <Text>{timestamp}</Text>} + </DateParser> + )} + </td> + <HiddenCell> + {!invitation.hasAnswer && ( + <PersonInvitation + {...invitation} + onResend={onResendReviewerInvite} + onRevoke={onRevokeReviewerInvite} + /> + )} + </HiddenCell> + </TableRow> + ))} + </tbody> + </Table> + ) -const reviewers = [ - { - id: 1, - fullName: 'Gica Hagi', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 2, - fullName: 'Cosmin Contra', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 11, - fullName: 'Gica Hagi', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 21, - fullName: 'Cosmin Contra', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 12, - fullName: 'Gica Hagi', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 22, - fullName: 'Cosmin Contra', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 13, - fullName: 'Gica Hagi', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 23, - fullName: 'Cosmin Contra', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 113, - fullName: 'Gica Hagi', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 213, - fullName: 'Cosmin Contra', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 123, - fullName: 'Gica Hagi', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, - { - id: 223, - fullName: 'Cosmin Contra', - invitedOn: Date.now(), - respondedOn: Date.now(), - submittedOn: Date.now() - 3000000000, - }, -] - -const ReviewersTable = () => ( - <Table> - <thead> - <tr> - <th> - <Label>Full Name</Label> - </th> - <th> - <Label>Invited on</Label> - </th> - <th> - <Label>Responded on</Label> - </th> - <th> - <Label>Submitted on</Label> - </th> - <th> </th> - </tr> - </thead> - <tbody> - {reviewers.map((r, index) => ( - <TableRow key={r.id}> - <td> - <Text>{r.fullName}</Text> - <Text customId ml={1}>{`Reviewer ${index + 1}`}</Text> - </td> - <td> - <DateParser timestamp={r.invitedOn}> - {timestamp => <Text>{timestamp}</Text>} - </DateParser> - </td> - <td> - <DateParser timestamp={r.respondedOn}> - {timestamp => <Text>{timestamp}</Text>} - </DateParser> - <Text ml={1} secondary> - ACCEPTED - </Text> - </td> - <td> - <DateParser timestamp={r.submittedOn}> - {timestamp => <Text>{timestamp}</Text>} - </DateParser> - </td> - <HiddenCell> - <PersonInvitation {...invitation} /> - </HiddenCell> - </TableRow> - ))} - </tbody> - </Table> -) - -export default ReviewersTable +export default shouldUpdate(() => false)(ReviewersTable) // #region styles const Table = styled.table` @@ -169,6 +90,7 @@ const Table = styled.table` & thead { border-bottom: 1px solid ${th('colorBorder')}; + background-color: ${th('colorBackgroundHue2')}; } & th, @@ -185,6 +107,7 @@ const Table = styled.table` const HiddenCell = styled.td` opacity: 0; + padding-top: ${th('gridUnit')}; ` const TableRow = styled.tr` diff --git a/packages/component-faraday-ui/src/WizardAuthors.js b/packages/component-faraday-ui/src/WizardAuthors.js index 3d951ec7aa7f910d3332e7584172e340a76b4e8e..3be5cad89501e670032295e0efa1937d704af0f7 100644 --- a/packages/component-faraday-ui/src/WizardAuthors.js +++ b/packages/component-faraday-ui/src/WizardAuthors.js @@ -53,7 +53,6 @@ const WizardAuthors = ({ isFetching, }) => ( <Fragment> - {isFetching && <h1>loading...</h1>} <Row alignItems="center" justify="flex-start"> <Item> <Label>Authors</Label> diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js index 50ae31b1d8799a3576d7c4e00f77fda1ada7d463..7cf367f7ec049a39afbd533ea780e74b9182e7e7 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js @@ -1,41 +1,24 @@ import React, { Fragment } from 'react' +import { H4 } from '@pubsweet/ui' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' import { - Tag, Tabs, - Label, marginHelper, ContextualBox, ReviewersTable, - ReviewerReport, InviteReviewers, ReviewerBreakdown, } from '../' -const report = { - submittedOn: Date.now(), - recommendation: 'Reject', - report: `Of all of the celestial bodies that capture our attention and - fascination as astronomers, none has a greater influence on life on - planet Earth than it’s own satellite, the moon. When you think about - it, we regard the moon with such powerful significance that unlike the - moons of other planets which we give names, we only refer to our one - and only orbiting orb as THE moon. It is not a moon. To us, it is the - one and only moon.`, - reviewer: { - fullName: 'Kenny Hudson', - reviewerNumber: 1, - }, - confidentialNote: `First 10 pages feel very familiar, you should check for plagarism.`, - files: [ - { id: 'file1', name: 'file1.pdf', size: 12356 }, - { id: 'file2', name: 'file2.pdf', size: 76421 }, - ], -} - -const ReviewerDetails = ({ fragment, onInviteReviewer }) => ( +const ReviewerDetails = ({ + fragment, + invitations, + onInviteReviewer, + onResendReviewerInvite, + onRevokeReviewerInvite, +}) => ( <ContextualBox label="Reviewer details" rightChildren={<ReviewerBreakdown fitContent fragment={fragment} mr={1} />} @@ -51,16 +34,7 @@ const ReviewerDetails = ({ fragment, onInviteReviewer }) => ( onClick={() => changeTab(0)} selected={selectedTab === 0} > - <Label>Reviewer Details</Label> - </TabButton> - <TabButton - ml={1} - mr={1} - onClick={() => changeTab(1)} - selected={selectedTab === 1} - > - <Label>Reviewer Reports</Label> - <Tag ml={1}>12</Tag> + <H4>Reviewer Details</H4> </TabButton> </TabsHeader> {selectedTab === 0 && ( @@ -69,14 +43,11 @@ const ReviewerDetails = ({ fragment, onInviteReviewer }) => ( modalKey="invite-reviewers" onInvite={onInviteReviewer} /> - <ReviewersTable /> - </Fragment> - )} - {selectedTab === 1 && ( - <Fragment> - <ReviewerReport report={report} /> - <ReviewerReport report={report} /> - <ReviewerReport report={report} /> + <ReviewersTable + invitations={invitations} + onResendReviewerInvite={onResendReviewerInvite} + onRevokeReviewerInvite={onRevokeReviewerInvite} + /> </Fragment> )} </Fragment> @@ -101,16 +72,20 @@ const TabButton = styled.div` height: calc(${th('gridUnit')} * 5); ${marginHelper}; + + ${H4} { + padding: 0 ${th('gridUnit')}; + } ` const TabsHeader = styled.div` align-items: center; border-bottom: 1px solid ${th('colorFurniture')}; + background-color: ${th('colorBackgroundHue2')}; box-sizing: border-box; display: flex; justify-content: flex-start; - margin-bottom: ${th('gridUnit')}; padding: 0 calc(${th('gridUnit')} * 3); ` // #endregion diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index 58cc658f4674169f5bb7b1ad477b73abd4897183..d5dc9fe1283200362165b066eb17153234ec756b 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -47,6 +47,9 @@ const ManuscriptLayout = ({ heResponseExpanded, onHEResponse, onInviteReviewer, + invitationsWithReviewers, + onResendReviewerInvite, + onRevokeReviewerInvite, }) => ( <Root pb={1}> {!isEmpty(collection) && !isEmpty(fragment) ? ( @@ -111,10 +114,15 @@ const ManuscriptLayout = ({ /> )} - <ReviewerDetails - fragment={fragment} - onInviteReviewer={onInviteReviewer} - /> + {get(currentUser, 'permissions.canInviteReviewers', false) && ( + <ReviewerDetails + fragment={fragment} + invitations={invitationsWithReviewers} + onInviteReviewer={onInviteReviewer} + onResendReviewerInvite={onResendReviewerInvite} + onRevokeReviewerInvite={onRevokeReviewerInvite} + /> + )} </Fragment> ) : ( <Text>Loading...</Text> @@ -126,8 +134,8 @@ export default ManuscriptLayout // #region styles const Root = styled.div` - overflow-y: auto; - min-height: 50vh; + overflow-y: visible; + min-height: 70vh; ${paddingHelper}; ` // #endregion diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 44b592715497dad6e1408c5ffb8ec9ada9c821bb..ed5bab6a31ab79490630375ad50106c68cfc848a 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -24,6 +24,7 @@ import { import { getSignedUrl } from 'pubsweet-components-faraday/src/redux/files' import { inviteReviewer, + revokeReviewer, reviewerDecision, } from 'pubsweet-components-faraday/src/redux/reviewers' import { @@ -37,10 +38,12 @@ import { canMakeRevision, canMakeDecision, canEditManuscript, + canInviteReviewers, pendingHEInvitation, currentUserIsReviewer, canMakeRecommendation, canOverrideTechnicalChecks, + getInvitationsWithReviewersForFragment, } from 'pubsweet-component-faraday-selectors' import { RemoteOpener } from 'pubsweet-component-faraday-ui' @@ -91,6 +94,7 @@ export default compose( assignHandlingEditor, createRecommendation, revokeHandlingEditor, + getUsers: actions.getUsers, getFragment: actions.getFragment, getCollection: actions.getCollection, updateVersion: actions.updateFragment, @@ -110,6 +114,7 @@ export default compose( isInvitedHE: !isUndefined(pendingHEInvitation), permissions: { canAssignHE: canAssignHE(state, match.params.project), + canInviteReviewers: canInviteReviewers(state, collection), canMakeRevision: canMakeRevision(state, collection, fragment), canMakeDecision: canMakeDecision(state, collection, fragment), canEditManuscript: canEditManuscript(state, collection, fragment), @@ -129,6 +134,10 @@ export default compose( eicDecision: getFormValues('eic-decision')(state), heInvitation: getFormValues('he-answer-invitation')(state), }, + invitationsWithReviewers: getInvitationsWithReviewersForFragment( + state, + get(fragment, 'id', ''), + ), }), ), ConnectPage(({ currentUser }) => { @@ -140,12 +149,14 @@ export default compose( withHandlers({ fetchUpdatedCollection: ({ fragment, + getUsers, collection, getFragment, getCollection, }) => () => { getCollection({ id: collection.id }) getFragment(collection, fragment) + getUsers() }, }), withHandlers({ @@ -256,6 +267,51 @@ export default compose( setModalError('Something went wrong...') }) }, + onResendReviewerInvite: ({ + fragment, + collection, + fetchUpdatedCollection, + }) => (email, { hideModal, setFetching, setModalError }) => { + setFetching(true) + inviteReviewer({ + reviewerData: { + email, + role: 'reviewer', + }, + fragmentId: fragment.id, + collectionId: collection.id, + }) + .then(() => { + setFetching(false) + hideModal() + fetchUpdatedCollection() + }) + .catch(() => { + setFetching(false) + setModalError('Something went wrong...') + }) + }, + onRevokeReviewerInvite: ({ + fragment, + collection, + fetchUpdatedCollection, + }) => (invitationId, { hideModal, setFetching, setModalError }) => { + setFetching(true) + revokeReviewer({ + invitationId, + fragmentId: fragment.id, + collectionId: collection.id, + }) + .then(() => { + setFetching(false) + hideModal() + fetchUpdatedCollection() + }) + .catch(() => { + setFetching(false) + setModalError('Something went wrong...') + }) + }, }), fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ toggleAssignHE: toggle, diff --git a/packages/components-faraday/src/redux/reviewers.js b/packages/components-faraday/src/redux/reviewers.js index 1c3212d7f4b913eb20b7ea06116934cd3c615900..204911271f2ae06fc53298afd4bbe7912705a493 100644 --- a/packages/components-faraday/src/redux/reviewers.js +++ b/packages/components-faraday/src/redux/reviewers.js @@ -31,19 +31,6 @@ const INVITE_REVIEWER_REQUEST = 'INVITE_REVIEWER_REQUEST' const INVITE_REVIEWER_SUCCESS = 'INVITE_REVIEWER_SUCCESS' const INVITE_REVIEWER_ERROR = 'INVITE_REVIEWER_ERROR' -const inviteRequest = () => ({ - type: INVITE_REVIEWER_REQUEST, -}) - -const inviteSuccess = () => ({ - type: INVITE_REVIEWER_SUCCESS, -}) - -const inviteError = error => ({ - type: INVITE_REVIEWER_ERROR, - error, -}) - // reviewer decision constants and action creators const REVIEWER_DECISION_REQUEST = 'REVIEWER_DECISION_REQUEST' const REVIEWER_DECISION_ERROR = 'REVIEWER_DECISION_ERROR' @@ -133,22 +120,10 @@ export const setReviewerPassword = reviewerBody => dispatch => { }) } -export const revokeReviewer = ( - invitationId, - collectionId, - fragmentId, -) => dispatch => { - dispatch(inviteRequest()) - return remove( +export const revokeReviewer = ({ fragmentId, collectionId, invitationId }) => + remove( `/collections/${collectionId}/fragments/${fragmentId}/invitations/${invitationId}`, - ).then( - () => dispatch(inviteSuccess()), - err => { - dispatch(inviteError(get(JSON.parse(err.response), 'error'))) - throw err - }, ) -} // #endregion // #region Actions - decision diff --git a/packages/xpub-faraday/app/FaradayApp.js b/packages/xpub-faraday/app/FaradayApp.js index 2494ed1003798b82ac4c6f075bd04adfc3ee175c..565963319b75d0616677c2bb2a53cbb96b7e6eb0 100644 --- a/packages/xpub-faraday/app/FaradayApp.js +++ b/packages/xpub-faraday/app/FaradayApp.js @@ -118,7 +118,7 @@ const appBarPaddingHelper = props => const MainContainer = styled.div` display: flex; flex-direction: column; - overflow-y: auto; + overflow-y: visible; ${appBarPaddingHelper}; ` // #endregion