diff --git a/README.md b/README.md index e3417878e63f365689df347de9f02fb0efcb9fbf..7099c5b1b78c887d30e06463b54698871abdc115 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -Note: xpub is still _very_ new. This repository contains an initial set of components but is not yet ready for use. ## xPub-faraday @@ -7,13 +6,11 @@ An MVP implementation of the first design sessions which allows a user to go thr ## Roadmap The major tasks we're planning to work on are the following: -* Implement a future-proof theming setup. (#88) -* Let users go through multiple rounds of review. (#50) -* Implement roles and permissions. (#58) -* Change the data model to account for the changes that have occured in `pubsweet-server`, as well as to make it easily portable to Postgres in the future. (#67) -* Merge xpub's authentication, routing and navigation with pubsweet's. (#55 #89 #57) - - +* Implement a future-proof theming setup. +* Let users go through multiple rounds of review. +* Implement roles and permissions. +* Change the data model to account for the changes that have occured in `pubsweet-server`, as well as to make it easily portable to Postgres in the future. +* Merge xpub's authentication, routing and navigation with pubsweet's. You can follow more fine-grained lists of things that we're working on * [Faraday](https://gitlab.coko.foundation/xpub/xpub-faraday/boards) for tasks related to `xpub-faraday` and @@ -21,7 +18,7 @@ You can follow more fine-grained lists of things that we're working on ## Installing -In the root directory, run `yarn` to install all the dependencies. +In the root directory, run `yarn` to install all the dependencies. ## Configuration Add the following values to `packages/xpub-collabra/config/local-development.json` @@ -34,10 +31,16 @@ Add the following values to `packages/xpub-collabra/config/local-development.jso } ``` +xPub-faraday is using external services as AWS, MTS-FTP, Publons, ORCID. In order to run the app locally a `.env` file is mandatory with keys and settings for each service. + +Contact us at technology@hindawi.com for help getting setup. + ## Running the app -1. `cd packages/xpub-faraday` -2. The first time you run the app, initialize the database with `yarn run setupdb` (press Enter when asked for a collection title, to skip that step). +1. Open Docker engine +2. `cd packages/xpub-faraday` +3. start services with `yarn services` +3. The first time you run the app, initialize the database with `yarn run setupdb` (press Enter when asked for a collection title, to skip that step). 3. `yarn start` diff --git a/packages/component-faraday-selectors/src/index.js b/packages/component-faraday-selectors/src/index.js index 925aac7a881fce50bf971f765a560042755bf468..fab299296f67b3f089781024a0c9a37c4dd7ed81 100644 --- a/packages/component-faraday-selectors/src/index.js +++ b/packages/component-faraday-selectors/src/index.js @@ -53,6 +53,7 @@ const cannotViewReviewersDetails = [ 'submitted', 'heInvited', ] + export const canViewReviewersDetails = (state, collection = {}) => { if (cannotViewReviewersDetails.includes(get(collection, 'status', 'draft'))) { return false @@ -60,6 +61,42 @@ export const canViewReviewersDetails = (state, collection = {}) => { return canViewReports(state, collection.id) } +const canHeViewEditorialCommentsStatuses = [ + 'revisionRequested', + 'rejected', + 'accepted', + 'inQA', + 'pendingApproval', +] +export const canHeViewEditorialComments = (state, collection = {}) => { + const isHE = isHEToManuscript(state, collection.id) + const status = get(collection, 'status', 'draft') + return isHE && canHeViewEditorialCommentsStatuses.includes(status) +} + +const canEICViewEditorialCommentsStatuses = ['rejected', 'accepted', 'inQA'] +export const canEICViewEditorialComments = (state, collection = {}) => { + const isEIC = currentUserIs(state, 'adminEiC') + const status = get(collection, 'status', 'draft') + return isEIC && canEICViewEditorialCommentsStatuses.includes(status) +} + +export const canViewEditorialComments = ( + state, + collection = {}, + fragmentId, +) => { + const editorialRecommentations = getFragmentEditorialComments( + state, + fragmentId, + ) + return ( + (canHeViewEditorialComments(state, collection) || + canEICViewEditorialComments(state, collection)) && + editorialRecommentations.length > 0 + ) +} + export const getUserToken = ({ currentUser }) => get(currentUser, 'user.token', '') @@ -254,6 +291,10 @@ export const getFragmentReviewerRecommendations = (state, fragmentId) => getFragmentRecommendations(state, fragmentId).filter( r => r.recommendationType === 'review', ) +const getFragmentEditorialComments = (state, fragmentId) => + getFragmentRecommendations(state, fragmentId).filter( + r => r.recommendationType === 'editorRecommendation', + ) const getOwnRecommendations = (state, fragmentId) => chain(state) diff --git a/packages/component-faraday-ui/src/EditorialReportCard.js b/packages/component-faraday-ui/src/EditorialReportCard.js index a294c039795914fe5e5333e95b395f938ee4f187..a89ca77499af8c0d0a6b6ceff3b6b5af97c617d5 100644 --- a/packages/component-faraday-ui/src/EditorialReportCard.js +++ b/packages/component-faraday-ui/src/EditorialReportCard.js @@ -9,26 +9,32 @@ import { Label, Item, Row, Text, Tag } from './' import { getReportComments } from './helpers' const EditorialReportCard = ({ + publicLabel, + privateLabel, journal, publicReport, privateReport, recommendation, - reviewerName, - reviewerRole, + editorName, + editorRole, report: { createdOn, reviewer }, }) => ( <Root> <Row justify="space-between" mb={2}> <Item vertical> - <Label mb={1 / 2}>Decision</Label> + {editorRole === 'HE' ? ( + <Label mb={1 / 2}>Recommendation</Label> + ) : ( + <Label mb={1 / 2}>Decision</Label> + )} <Text>{recommendation}</Text> </Item> <Item justify="flex-end"> {reviewer && ( <Fragment> - <Text mr={1 / 2}>{reviewerName}</Text> - <Tag mr={2}>{reviewerRole}</Tag> + <Text mr={1 / 2}>{editorName}</Text> + <Tag mr={2}>{editorRole}</Tag> </Fragment> )} <DateParser timestamp={createdOn}> @@ -40,7 +46,7 @@ const EditorialReportCard = ({ {publicReport && ( <Row mb={2}> <Item vertical> - <Label mb={1 / 2}>Message For Author</Label> + <Label mb={1 / 2}>{publicLabel}</Label> <Text>{publicReport}</Text> </Item> </Row> @@ -49,7 +55,7 @@ const EditorialReportCard = ({ {privateReport && ( <Row mb={2}> <Item vertical> - <Label mb={1 / 2}>Message For Editorial Team</Label> + <Label mb={1 / 2}>{privateLabel}</Label> <Text>{privateReport}</Text> </Item> </Row> @@ -85,8 +91,8 @@ export default compose( recommendation: getRecommendationLabel(), publicReport: getReportComments({ report, isPublic: true }), privateReport: getReportComments({ report, isPublic: false }), - reviewerName: getReviewerName(), - reviewerRole: getReviewerRole(), + editorName: getReviewerName(), + editorRole: getReviewerRole(), }), ), )(EditorialReportCard) diff --git a/packages/component-faraday-ui/src/EditorialReportCard.md b/packages/component-faraday-ui/src/EditorialReportCard.md index 26fd9dce55e7c917e5944b6baf617a3384c50407..0c81711ef2c425f242e7f7b7433347f78de2aa15 100644 --- a/packages/component-faraday-ui/src/EditorialReportCard.md +++ b/packages/component-faraday-ui/src/EditorialReportCard.md @@ -47,7 +47,12 @@ const journal = { }, ], } -;<EditorialReportCard report={report} journal={journal} /> +;<EditorialReportCard + report={report} + journal={journal} + publicLabel="Message For Author" + privateLabel="Message For Editorial Team" + /> ``` Card with message for the editorial team @@ -97,7 +102,12 @@ const journal = { }, ], } -;<EditorialReportCard report={report} journal={journal} /> +;<EditorialReportCard + report={report} + journal={journal} + publicLabel="Message For Author" + privateLabel="Message For Editorial Team" + /> ``` Card with message for the editorial team and for the author @@ -152,5 +162,11 @@ const journal = { }, ], } -;<EditorialReportCard report={report} journal={journal} /> + +;<EditorialReportCard + report={report} + journal={journal} + publicLabel="Message For Author" + privateLabel="Message For Editorial Team" + /> ``` diff --git a/packages/component-faraday-ui/src/PublonsTable.js b/packages/component-faraday-ui/src/PublonsTable.js new file mode 100644 index 0000000000000000000000000000000000000000..ed2ffa47ad5bface693c3f215619034c91ffce86 --- /dev/null +++ b/packages/component-faraday-ui/src/PublonsTable.js @@ -0,0 +1,156 @@ +import React, { Fragment } from 'react' +import { get, last, head } from 'lodash' +import styled from 'styled-components' +import { th } from '@pubsweet/ui-toolkit' +import { Button, Spinner } from '@pubsweet/ui' +import { compose, withHandlers, withProps } from 'recompose' + +import { Label, OpenModal, Text, withFetching } from '../' + +const TableView = ({ + reviewers, + onInviteReviewer, + setFetching, + isFetching, + publonsError, +}) => { + if (publonsError) { + return ( + <Text align="center" error> + {publonsError} + </Text> + ) + } + return reviewers.length === 0 ? ( + <Text align="center">No suggestions yet.</Text> + ) : ( + <Table> + <thead> + <tr> + <th> + <Label>Full Name</Label> + </th> + <th> + <Label>Affiliation</Label> + </th> + <th> + <Label>No. of Reviews</Label> + </th> + <th> </th> + </tr> + </thead> + <tbody> + {reviewers.map(reviewer => ( + <TableRow key={reviewer.email}> + <td> + <Text>{`${get(reviewer, 'name', '')}`}</Text> + </td> + <td> + <Text>{`${get(reviewer, 'affiliation', '')}`}</Text> + </td> + <td> + <Text>{`${get(reviewer, 'reviews', '')}`}</Text> + </td> + <HiddenCell> + <OpenModal + confirmText="Invite" + isFetching={isFetching} + onConfirm={modalProps => onInviteReviewer(reviewer, modalProps)} + setFetching={setFetching} + title="Send invitation to review?" + > + {showModal => ( + <Button onClick={showModal} primary size="small"> + SEND + </Button> + )} + </OpenModal> + </HiddenCell> + </TableRow> + ))} + </tbody> + </Table> + ) +} + +const PublonsTable = ({ publonsFetching, ...rest }) => ( + <Fragment>{publonsFetching ? <Spinner /> : <TableView {...rest} />}</Fragment> +) + +export default compose( + withFetching, + withProps(({ reviewers = [] }) => ({ + reviewers, + })), + withHandlers({ + onInviteReviewer: ({ onInvite }) => (reviewer, modalProps) => { + const names = reviewer.name.split(' ') + const newReviewer = { + email: reviewer.email, + role: 'reviewer', + firstName: head(names), + lastName: last(names), + } + onInvite(newReviewer, modalProps) + }, + }), +)(PublonsTable) + +// #region styles +const Table = styled.table` + border-collapse: collapse; + + & thead { + border: 1px solid ${th('colorBorder')}; + background-color: ${th('colorBackgroundHue2')}; + padding-top: calc(${th('gridUnit')} * 2); + } + + & th, + & td { + border: none; + padding-left: calc(${th('gridUnit')} * 2); + text-align: start; + vertical-align: middle; + + height: calc(${th('gridUnit')} * 5); + min-width: calc(${th('gridUnit')} * 12); + } +` + +const HiddenCell = styled.td` + opacity: 0; + padding-top: ${th('gridUnit')}; +` + +const HidableCell = styled.td` + opacity: 1; + padding-top: ${th('gridUnit')}; +` + +const TableRow = styled.tr` + background-color: ${th('colorBackgroundHue2')}; + border-bottom: 1px solid ${th('colorBorder')}; + + & td:first-child { + min-width: calc(${th('gridUnit')} * 20); + } + + & td:last-child { + vertical-align: top; + text-align: right; + padding-right: calc(8px * 2); + } + + &:hover { + background: ${th('colorBackgroundHue3')}; + + ${HiddenCell} { + opacity: 1; + } + ${HidableCell} { + opacity: 0; + } + } +` +// #endregion diff --git a/packages/component-faraday-ui/src/PublonsTable.md b/packages/component-faraday-ui/src/PublonsTable.md new file mode 100644 index 0000000000000000000000000000000000000000..f692c2e4740672589b4ff386c06e1f13c180315e --- /dev/null +++ b/packages/component-faraday-ui/src/PublonsTable.md @@ -0,0 +1,39 @@ +A list of publon reviewers. + +```js +const reviewers = [ + { + id: 0, + email: 'email1@email.com', + publishingName: 'Name1', + recentOrganizations: { + name: 'Org1' + }, + numVerifiedReviews: '100' + }, + { + id: 1, + email: 'email2@email.com', + publishingName: 'Name2', + recentOrganizations: { + name: 'Org2' + }, + numVerifiedReviews: '200' + }, + { + id: 2, + email: 'email3@email.com', + publishingName: 'Name3', + recentOrganizations: { + name: 'Org3' + }, + numVerifiedReviews: '300' + }, +]; + +<PublonsTable reviewers={reviewers} onInviteReviwer={(reviewer, modalProps) => { + console.log('the reviewer', reviewer) + + modalProps.setModalError('avem eroare boss') +}}/> +``` diff --git a/packages/component-faraday-ui/src/ReviewersTable.js b/packages/component-faraday-ui/src/ReviewersTable.js index 901e657ebefcad468a49c2801c0485c6a7de8def..9f9e2aaa75309afd25084e94a65cbcf65e2c646b 100644 --- a/packages/component-faraday-ui/src/ReviewersTable.js +++ b/packages/component-faraday-ui/src/ReviewersTable.js @@ -83,7 +83,9 @@ const ReviewersTable = ({ </tbody> </Table> ) : ( - <Text align="center">No reviewers invited yet.</Text> + <Text align="center" pb={2} pt={2}> + No reviewers invited yet. + </Text> ) const orderInvitations = i => { diff --git a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js index d64a8a58c009bf3d77b62b2f5e2367b371d389c9..8e242820b8a3077267b79efe84b7355b6d7029d0 100644 --- a/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js +++ b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js @@ -12,6 +12,7 @@ import { marginHelper, ContextualBox, ReviewersTable, + PublonsTable, ReviewerReport, InviteReviewers, ReviewerBreakdown, @@ -24,10 +25,14 @@ const ReviewerDetails = ({ reports = [], fragment, invitations, + publonReviewers, + isFetching, previewFile, downloadFile, + fetchingError, canInviteReviewers, onInviteReviewer, + onInvitePublonReviewer, onResendReviewerInvite, onRevokeReviewerInvite, toggle, @@ -59,7 +64,16 @@ const ReviewerDetails = ({ > <H4>Reviewer Details</H4> </TabButton> - + {canInviteReviewers && ( + <TabButton + ml={1} + mr={1} + onClick={() => changeTab(2)} + selected={selectedTab === 2} + > + <H4>Reviewer Suggestions</H4> + </TabButton> + )} <TabButton ml={1} mr={1} @@ -86,6 +100,14 @@ const ReviewerDetails = ({ /> </Fragment> )} + {selectedTab === 2 && ( + <PublonsTable + onInvite={onInvitePublonReviewer} + publonsError={fetchingError} + publonsFetching={isFetching} + reviewers={publonReviewers} + /> + )} {selectedTab === 1 && ( <Fragment> {reports.length === 0 && ( @@ -114,14 +136,22 @@ const ReviewerDetails = ({ export default compose( withFilePreview, withFileDownload, - withProps(({ invitations = [], reviewerReports = [], currentUser }) => ({ - token: get(currentUser, 'token', ''), - invitations: invitations.map(i => ({ - ...i, - review: reviewerReports.find(r => r.userId === i.userId), - })), - reports: reviewerReports.filter(r => r.submittedOn), - })), + withProps( + ({ + invitations = [], + publonReviewers = [], + reviewerReports = [], + currentUser, + }) => ({ + token: get(currentUser, 'token', ''), + publonReviewers, + invitations: invitations.map(i => ({ + ...i, + review: reviewerReports.find(r => r.userId === i.userId), + })), + reports: reviewerReports.filter(r => r.submittedOn), + }), + ), withProps(({ currentUser }) => ({ canInviteReviewers: get(currentUser, 'permissions.canInviteReviewers'), canViewReviewersDetails: get( diff --git a/packages/component-faraday-ui/src/index.js b/packages/component-faraday-ui/src/index.js index 9c505e4d598f0482be74d0c8d9de774572327b8d..263820a923f836e555affe74bed8bd51d6ea7b6d 100644 --- a/packages/component-faraday-ui/src/index.js +++ b/packages/component-faraday-ui/src/index.js @@ -30,6 +30,7 @@ export { default as Pagination } from './Pagination' export { default as PersonInfo } from './PersonInfo' export { default as PersonInvitation } from './PersonInvitation' export { default as PreviewFile } from './PreviewFile' +export { default as PublonsTable } from './PublonsTable' export { default as RadioWithComments } from './RadioWithComments' export { default as ReviewerReport } from './ReviewerReport' export { default as ReviewersTable } from './ReviewersTable' diff --git a/packages/component-faraday-ui/src/modals/MultiAction.js b/packages/component-faraday-ui/src/modals/MultiAction.js index d8359edec7d8bd613a5a8401ba4629092429ee8f..f441ad6ec0ac2d61aaa4fa968ac4d7648ef26d38 100644 --- a/packages/component-faraday-ui/src/modals/MultiAction.js +++ b/packages/component-faraday-ui/src/modals/MultiAction.js @@ -27,7 +27,7 @@ const MultiAction = ({ )} {renderContent()} {modalError && ( - <Text error mt={1}> + <Text align="center" error mt={1}> {modalError} </Text> )} @@ -83,7 +83,7 @@ const Root = styled.div` flex-direction: column; position: relative; padding: calc(${th('gridUnit')} * 5); - width: calc(${th('gridUnit')} * 60); + width: calc(${th('gridUnit')} * 70); ${H2} { margin: 0 0 ${th('gridUnit')} 0; diff --git a/packages/component-invite/src/FragmentsInvitations.js b/packages/component-invite/src/FragmentsInvitations.js index 7a4d2507424cbeb5a5a0c0357779508fed009a8d..7e01cf30948ac41226e63f44571199461c0f106a 100644 --- a/packages/component-invite/src/FragmentsInvitations.js +++ b/packages/component-invite/src/FragmentsInvitations.js @@ -16,7 +16,12 @@ const FragmentsInvitations = app => { * @apiParamExample {json} Body * { * "email": "email@example.com", - * "role": "reviewer", [acceptedValues: reviewer] + * "role": "reviewer", [acceptedValues: reviewer], + * "firstName": "Julien", + * "lastName": "Hopfenkonig", + * "affiliation": "UCLA", + * "country": "RO" + * "isPublons": false [Boolean] * } * @apiSuccessExample {json} Success * HTTP/1.1 200 OK @@ -51,8 +56,8 @@ const FragmentsInvitations = app => { * HTTP/1.1 200 OK * [{ * "name": "John Smith", - * "invitedOn": 1525428890167, - * "respondedOn": 1525428890299, + * "invitedOn": 1525428890167, + * "respondedOn": 1525428890299, * "email": "email@example.com", * "status": "pending", * "invitationId": "1990881" diff --git a/packages/component-invite/src/routes/fragmentsInvitations/post.js b/packages/component-invite/src/routes/fragmentsInvitations/post.js index b0935ef55d759310cdcc596e40e70353967370f9..2eab3d2d1642e1aed6f2a9384bbf11b9058fd993 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/post.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js @@ -1,4 +1,5 @@ const logger = require('@pubsweet/logger') + const { Team, User, @@ -122,10 +123,33 @@ module.exports = models => async (req, res) => { } catch (e) { const userHelper = new User({ UserModel }) - const newUser = await userHelper.createUser({ - role, - body: req.body, - }) + const userData = req.body + const { firstName = '', lastName = '', isPublons } = userData + if (!services.checkForUndefinedParams(firstName, lastName)) { + return res + .status(400) + .json({ error: 'First name and last name are required.' }) + } + + if (isPublons && process.env.PUBLONS_MOCK_EMAIL) { + const mockEmail = process.env.PUBLONS_MOCK_EMAIL + userData.email = mockEmail.replace( + '__NAME__', + `${firstName.trim()}.${lastName.trim()}`, + ) + } + + let newUser + try { + newUser = await userHelper.createUser({ + role, + body: userData, + }) + } catch (e) { + return res + .status(400) + .json({ error: `User already exists with email: ${userData.email}` }) + } if (collection.status === 'heAssigned') await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) diff --git a/packages/component-manuscript/src/components/EditorialCommentCard.js b/packages/component-manuscript/src/components/EditorialCommentCard.js index 67c882a3bb8257f6ce5129c1b320fe1f1aa7686d..366200e4fba723bc2c2a7431b4d19b39ea0501b9 100644 --- a/packages/component-manuscript/src/components/EditorialCommentCard.js +++ b/packages/component-manuscript/src/components/EditorialCommentCard.js @@ -15,7 +15,13 @@ const EditorialCommentCard = ({ journal, reports = [], toggle, expanded }) => ( toggle={toggle} > {reports.map(report => ( - <EditorialReportCard journal={journal} key={report.id} report={report} /> + <EditorialReportCard + journal={journal} + key={report.id} + privateLabel="Message For Editorial Team" + publicLabel="Message For Author" + report={report} + /> ))} </ContextualBox> ) diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index dda691a1818b1bf2f31f221f257501684cb566ad..6c1cfc840e633a7eb86a685e7b0f7f2316eae15c 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -38,6 +38,7 @@ const ManuscriptLayout = ({ fragment = {}, changeForm, isFetching, + fetchingError, formValues, heExpanded, onHEResponse, @@ -50,13 +51,13 @@ const ManuscriptLayout = ({ onRevokeReviewerInvite, toggleReviewerResponse, invitationsWithReviewers, + publonReviewers, reviewerResponseExpanded, pendingOwnRecommendation, toggleReviewerRecommendations, reviewerRecommendationExpanded, shouldReview, submittedOwnRecommendation, - heAccepted, reviewerReports, onEditorialRecommendation, reviewerRecommendations, @@ -64,6 +65,7 @@ const ManuscriptLayout = ({ reviewerDetailsExpanded, toggleHeRecommendation, heRecommendationExpanded, + onInvitePublonReviewer, }) => ( <Root pb={30}> {!isEmpty(collection) && !isEmpty(fragment) ? ( @@ -95,15 +97,14 @@ const ManuscriptLayout = ({ getSignedUrl={getSignedUrl} /> - {get(currentUser, 'permissions.canViewReports', true) && - !!editorialRecommendations.length && ( - <EditorialCommentCard - expanded={heRecommendationExpanded} - journal={journal} - reports={editorialRecommendations} - toggle={toggleHeRecommendation} - /> - )} + {get(currentUser, 'permissions.canViewEditorialComments', true) && ( + <EditorialCommentCard + expanded={heRecommendationExpanded} + journal={journal} + reports={editorialRecommendations} + toggle={toggleHeRecommendation} + /> + )} {submittedOwnRecommendation && ( <ReviewerReportCard @@ -163,15 +164,19 @@ const ManuscriptLayout = ({ <ReviewerDetails currentUser={currentUser} expanded={reviewerDetailsExpanded} + fetchingError={fetchingError} fragment={fragment} getSignedUrl={getSignedUrl} highlight={reviewerReports.length === 0} invitations={invitationsWithReviewers} + isFetching={isFetching.publonsFetching} journal={journal} mb={2} + onInvitePublonReviewer={onInvitePublonReviewer} onInviteReviewer={onInviteReviewer} onResendReviewerInvite={onResendReviewerInvite} onRevokeReviewerInvite={onRevokeReviewerInvite} + publonReviewers={publonReviewers} reviewerReports={reviewerReports} scrollIntoView toggle={toggleReviewerDetails} diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 7436b922ec943bf0d891b1c19b24a3c17d6e1750..912cab3e23ec4fe707dffe55fe8e41b96d755d9f 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -48,6 +48,7 @@ import { currentUserIsReviewer, parseCollectionDetails, canMakeHERecommendation, + canViewEditorialComments, canViewReviewersDetails, pendingReviewerInvitation, canOverrideTechnicalChecks, @@ -56,10 +57,19 @@ import { getFragmentReviewerRecommendations, getInvitationsWithReviewersForFragment, } from 'pubsweet-component-faraday-selectors' -import { RemoteOpener, handleError } from 'pubsweet-component-faraday-ui' +import { + RemoteOpener, + handleError, + withFetching, +} from 'pubsweet-component-faraday-ui' import ManuscriptLayout from './ManuscriptLayout' -import { parseEicDecision, parseSearchParams, redirectToError } from './utils' +import { + parseEicDecision, + parseSearchParams, + redirectToError, + getPublonsReviewers, +} from './utils' import { canAssignHE, selectFetching, @@ -74,6 +84,8 @@ export default compose( setDisplayName('ManuscriptPage'), withJournal, withRouter, + withFetching, + withState('publonReviewers', 'setPublonsReviewers', []), withState('editorInChief', 'setEiC', 'N/A'), ConnectPage(({ match }) => [ actions.getCollection({ id: match.params.project }), @@ -136,6 +148,7 @@ export default compose( pendingHEInvitation, pendingOwnRecommendation, pendingReviewerInvitation, + isFetching, }, ) => ({ currentUser: { @@ -154,6 +167,11 @@ export default compose( }), canAssignHE: canAssignHE(state, match.params.project), canViewReports: canViewReports(state, match.params.project), + canViewEditorialComments: canViewEditorialComments( + state, + collection, + match.params.version, + ), canInviteReviewers: canInviteReviewers(state, collection), canMakeRecommendation: !isUndefined(pendingOwnRecommendation), canMakeRevision: canMakeRevision(state, collection, fragment), @@ -165,6 +183,7 @@ export default compose( }, isFetching: { editorsFetching: selectFetching(state), + publonsFetching: isFetching, }, formValues: { eicDecision: getFormValues('eic-decision')(state), @@ -196,6 +215,22 @@ export default compose( getFragment(collection, fragment) getUsers() }, + getPublonsReviewers: ({ clearError, setFetching, setPublonsReviewers }) => ( + fragmentId, + errorFn, + ) => { + clearError() + setFetching(true) + getPublonsReviewers(fragmentId) + .then(res => { + setFetching(false) + setPublonsReviewers(res) + }) + .catch(e => { + setFetching(false) + handleError(errorFn)(e) + }) + }, }), withHandlers({ updateManuscript: ({ updateVersion, collection, fragment }) => data => @@ -403,6 +438,35 @@ export default compose( }) }, }), + withHandlers({ + onInvitePublonReviewer: ({ + setError, + fragment, + collection, + clearError, + getPublonsReviewers, + fetchUpdatedCollection, + setFetching: setListFetching, + }) => (reviewerData, { hideModal, setModalError, setFetching }) => { + setFetching(true) + inviteReviewer({ + reviewerData, + isPublons: true, + fragmentId: fragment.id, + collectionId: collection.id, + }) + .then(() => { + setFetching(false) + hideModal() + fetchUpdatedCollection() + getPublonsReviewers(fragment.id) + }) + .catch(err => { + setFetching(false) + handleError(setModalError)(err) + }) + }, + }), fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ toggleAssignHE: toggle, heExpanded: expanded, @@ -438,11 +502,13 @@ export default compose( const { match, history, + setError, location, shouldReview, reviewerReports, setEditorInChief, clearCustomError, + getPublonsReviewers, hasManuscriptFailure, fetchUpdatedCollection, editorialRecommendations, @@ -451,8 +517,10 @@ export default compose( isInvitedToReview, isHEToManuscript, isEIC, + permissions: { canInviteReviewers }, }, } = this.props + if (hasManuscriptFailure) { history.push('/not-found') clearCustomError() @@ -472,6 +540,10 @@ export default compose( setEditorInChief(head(res.users)), ) + if (canInviteReviewers) { + getPublonsReviewers(fragmentId, setError) + } + if (isInvitedHE) { this.props.toggleHEResponse() } diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js index 4a3a4edef0f9f15156c27517ca4c0577ffd6b608..8d9a2d6cc3ad46d4304e2fc07dcfb57731c49c3b 100644 --- a/packages/component-manuscript/src/components/utils.js +++ b/packages/component-manuscript/src/components/utils.js @@ -13,6 +13,7 @@ import { change as changeForm } from 'redux-form' import { actions } from 'pubsweet-client/src' import { handleError } from 'pubsweet-component-faraday-ui' +import { get as apiGet } from 'pubsweet-client/src/helpers/api' import { autosaveRequest, @@ -210,6 +211,7 @@ export const onReviewSubmit = ( collectionId: project.id, recommendation: newValues, }) + .then(() => dispatch(actions.getCollection({ id: project.id }))) .then(() => { dispatch(actions.getFragments({ id: project.id })) hideModal() @@ -275,7 +277,6 @@ export const onRevisionSubmit = ( onCancel: hideModal, }) } - // revision validators export const requiredHTML = value => { if (value && value.replace('<p></p>', '').replace('<h1></h1>', '')) { @@ -303,3 +304,7 @@ export const parseEicDecision = ({ decision, message }) => ({ }, ], }) + +// handle publons +export const getPublonsReviewers = fragmentId => + apiGet(`/fragments/${fragmentId}/publons`) diff --git a/packages/component-publons/src/routes/publons/get.js b/packages/component-publons/src/routes/publons/get.js index 2dcf6739e57c40f2626618b87560be3500564222..5335e50a30bce8def83c53daa5bc45b9cf72f0cc 100644 --- a/packages/component-publons/src/routes/publons/get.js +++ b/packages/component-publons/src/routes/publons/get.js @@ -40,7 +40,8 @@ module.exports = models => async (req, res) => { fragmentId: fragment.id, }) const parsedFragment = await fragmentHelper.getFragmentData() - const memberIds = await teamHelper.getTeamMembers({ + let memberIds = [] + memberIds = await teamHelper.getTeamMembers({ role: 'reviewer', objectType: 'fragment', }) diff --git a/packages/component-wizard/src/components/SubmissionStatement.js b/packages/component-wizard/src/components/SubmissionStatement.js index cdbc630458e7808c29651a8bd83520d476f30301..4769604cea17f535dd3b4fb239361d5bdc8b154d 100644 --- a/packages/component-wizard/src/components/SubmissionStatement.js +++ b/packages/component-wizard/src/components/SubmissionStatement.js @@ -3,23 +3,13 @@ import { ActionLink, Text, Row } from 'pubsweet-component-faraday-ui' const SubmissionStatement = () => ( <Fragment> - <Row mb={1}> + <Row mb={1} mt={2}> <Text> This manuscript is not currently submitted to or under consideration in any other journals. </Text> </Row> - <Row mb={1}> - <Text> - The submission ensures that sources are given proper attribution (the - journal employs <b>Crossref Similarity Check</b> to compare submissions - against published scholarly content. If, in the judgement of an editor, - a submission is genuinely suspected of plagiarism, it will be returned - to the author(s) with a request for explanation). - </Text> - </Row> - <Row mb={1}> <Text> The manuscript complies with all relevant{' '} @@ -29,28 +19,13 @@ const SubmissionStatement = () => ( </Text> </Row> - <Row mb={1}> - <Text> - If applicable - there is a Data Availability statement, containing - information about the location of any open data and materials in the - manuscript, and how others can access the data. - </Text> - </Row> - - <Row mb={1}> - <Text> - A conflict of interest statement is present in the manuscript, even if - to state there is no conflict of interest. - </Text> - </Row> - - <Row mb={1}> + <Row mb={3}> <Text> You have read and understood the{' '} <ActionLink to="https://www.hindawi.com/terms/"> terms of service - </ActionLink> - {' & '} + </ActionLink>{' '} + and{' '} <ActionLink to="https://www.hindawi.com/privacy/"> privacy policy </ActionLink>{' '} diff --git a/packages/components-faraday/src/components/SignUp/SignUpStep0.js b/packages/components-faraday/src/components/SignUp/SignUpStep0.js index 0c7e9ec11627bbee30c7d934bf6b4e458cd84a19..51edb75af1575d53e615c6cff9db1f7081b682ca 100644 --- a/packages/components-faraday/src/components/SignUp/SignUpStep0.js +++ b/packages/components-faraday/src/components/SignUp/SignUpStep0.js @@ -4,6 +4,7 @@ import { reduxForm } from 'redux-form' import { get, isUndefined } from 'lodash' import { required as requiredValidator } from 'xpub-validators' import { Menu, Button, Checkbox, TextField, ValidatedField } from '@pubsweet/ui' +import styled from 'styled-components' import { Row, @@ -16,7 +17,7 @@ import { } from 'pubsweet-component-faraday-ui' const AgreeCheckbox = ({ value, onChange }) => ( - <Row alignItems="center" justify="flex-start" mb={4} mt={3}> + <Row alignItems="center" justify="flex-start" mb={3} mt={3}> <Checkbox checked={value} onChange={onChange} value={value} /> <Text> I agree with the{' '} @@ -26,6 +27,9 @@ const AgreeCheckbox = ({ value, onChange }) => ( </Text> </Row> ) +const ValidationRow = styled(Row)` + justify-content: left; +` const Step0 = ({ type, @@ -98,11 +102,13 @@ const Step0 = ({ </Item> </Row> - <ValidatedField - component={AgreeCheckbox} - name="agreeTC" - validate={[requiredValidator]} - /> + <ValidationRow mb={2}> + <ValidatedField + component={AgreeCheckbox} + name="agreeTC" + validate={[requiredValidator]} + /> + </ValidationRow> <Row> <Text secondary small> diff --git a/packages/components-faraday/src/redux/reviewers.js b/packages/components-faraday/src/redux/reviewers.js index 7542b980da4cdb1aaab21256f0b85bca2cabfb3d..4cb9dd209e6d8d922a0e3ca394698743754b61c2 100644 --- a/packages/components-faraday/src/redux/reviewers.js +++ b/packages/components-faraday/src/redux/reviewers.js @@ -59,10 +59,16 @@ export const getCollectionReviewers = (collectionId, fragmentId) => dispatch => ) // #endregion -export const inviteReviewer = ({ reviewerData, collectionId, fragmentId }) => +export const inviteReviewer = ({ + fragmentId, + reviewerData, + collectionId, + isPublons = false, +}) => create(`/collections/${collectionId}/fragments/${fragmentId}/invitations`, { ...reviewerData, role: 'reviewer', + isPublons, }) // #region Actions - invitations diff --git a/packages/xpub-faraday/config/authsome-helpers.js b/packages/xpub-faraday/config/authsome-helpers.js index f44cf22496e43c2a4428dfa51e2ee4acf99c0d91..c97f8b89a8f9acb1b7b626e5fc5b79636889bf65 100644 --- a/packages/xpub-faraday/config/authsome-helpers.js +++ b/packages/xpub-faraday/config/authsome-helpers.js @@ -180,6 +180,17 @@ const stripeFragmentByRole = ({ ? recommendations.filter(r => r.userId === user.id) : [], } + case 'handlingEditor': + return { + ...fragment, + recommendations: recommendations + ? recommendations.filter( + r => + r.submittedOn || + r.recommendationType === 'editorRecommendation', + ) + : [], + } default: return fragment } diff --git a/packages/xpub-faraday/tests/config/authsome-helpers.test.js b/packages/xpub-faraday/tests/config/authsome-helpers.test.js index 82e6978c47a3d5f509dae9d86b9f14d6b377eccd..58aa8b2b798660dda269a16c8819a9126adf24c0 100644 --- a/packages/xpub-faraday/tests/config/authsome-helpers.test.js +++ b/packages/xpub-faraday/tests/config/authsome-helpers.test.js @@ -88,81 +88,129 @@ describe('Authsome Helpers', () => { expect(result.handlingEditor).toBeFalsy() }) - it('stripeFragment - reviewer should not see authors email', () => { - const { fragment } = testFixtures.fragments - const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' }) - const { authors = [] } = result - expect(authors[0].email).toBeFalsy() - }) - it('stripeFragment - other roles than reviewer should see authors emails', () => { - const { fragment } = testFixtures.fragments - const result = ah.stripeFragmentByRole({ fragment, role: 'author' }) - const { authors = [] } = result + describe('stripeFragmentByRole', () => { + it('reviewer should not see authors email', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' }) + const { authors = [] } = result + expect(authors[0].email).toBeFalsy() + }) + it('other roles than reviewer should see authors emails', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole({ fragment, role: 'author' }) + const { authors = [] } = result - expect(authors[0].email).toBeTruthy() - }) + expect(authors[0].email).toBeTruthy() + }) - it('stripeFragment - reviewer should not see cover letter', () => { - const { fragment } = testFixtures.fragments - const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' }) - const { files = {} } = result - expect(files.coverLetter).toBeFalsy() - }) - it('stripeFragment - reviewer should not see others reviews', () => { - const { fragment } = testFixtures.fragments - const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' }) - const { recommendations } = result - expect(recommendations).toEqual([]) - }) + it('reviewer should not see cover letter', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' }) + const { files = {} } = result + expect(files.coverLetter).toBeFalsy() + }) + it('reviewer should not see others reviews', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole({ fragment, role: 'reviewer' }) + const { recommendations } = result + expect(recommendations).toEqual([]) + }) - it('stripeFragment - author should not see recommendations if a decision has not been made', () => { - const { fragment } = testFixtures.fragments - fragment.recommendations = [ - { - comments: [ - { - content: 'private', - public: false, - }, - { - content: 'public', - public: true, - }, - ], - }, - ] - const { recommendations } = ah.stripeFragmentByRole({ - fragment, - role: 'author', - status: 'underReview', - isLast: true, - }) - expect(recommendations).toHaveLength(0) - }) - it('stripeFragment - author should see reviews only if recommendation has been made and only public ones', () => { - const { fragment } = testFixtures.fragments - fragment.recommendations = [ - { - comments: [ - { - content: 'private', - public: false, - }, - { - content: 'public', - public: true, - }, - ], - }, - ] - const result = ah.stripeFragmentByRole({ - fragment, - role: 'author', - status: 'revisionRequested', - }) - const publicComments = get(result, 'recommendations[0].comments') - expect(publicComments).toHaveLength(1) + it('author should not see recommendations if a decision has not been made', () => { + const { fragment } = testFixtures.fragments + fragment.recommendations = [ + { + comments: [ + { + content: 'private', + public: false, + }, + { + content: 'public', + public: true, + }, + ], + }, + ] + const { recommendations } = ah.stripeFragmentByRole({ + fragment, + role: 'author', + status: 'underReview', + isLast: true, + }) + expect(recommendations).toHaveLength(0) + }) + it('author should see reviews only if recommendation has been made and only public ones', () => { + const { fragment } = testFixtures.fragments + fragment.recommendations = [ + { + comments: [ + { + content: 'private', + public: false, + }, + { + content: 'public', + public: true, + }, + ], + }, + ] + const result = ah.stripeFragmentByRole({ + fragment, + role: 'author', + status: 'revisionRequested', + }) + const publicComments = get(result, 'recommendations[0].comments') + expect(publicComments).toHaveLength(1) + }) + it('HE should not see unsubmitted recommendations', () => { + const { fragment } = testFixtures.fragments + fragment.recommendations = [ + { + comments: [ + { + content: 'private', + public: false, + }, + { + content: 'public', + public: true, + }, + ], + }, + ] + const { recommendations } = ah.stripeFragmentByRole({ + fragment, + role: 'handlingEditor', + }) + expect(recommendations).toHaveLength(0) + }) + it('HE should see submitted recommendations', () => { + const { fragment } = testFixtures.fragments + fragment.recommendations = [ + { + comments: [ + { + content: 'private', + public: false, + }, + { + content: 'public', + public: true, + }, + ], + submittedOn: 1122333, + }, + ] + const { recommendations } = ah.stripeFragmentByRole({ + fragment, + role: 'handlingEditor', + }) + expect(recommendations).toHaveLength(1) + }) }) + it('getUsersList - should return parsed users when the user is admin', async () => { const { admin } = testFixtures.users const parsedUsers = await ah.getUsersList({