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-ui/src/PublonsTable.js b/packages/component-faraday-ui/src/PublonsTable.js new file mode 100644 index 0000000000000000000000000000000000000000..d11c7d479ecb15cc49cbfe9d7ef2938335e444ce --- /dev/null +++ b/packages/component-faraday-ui/src/PublonsTable.js @@ -0,0 +1,154 @@ +import React, { Fragment } from 'react' +import styled from 'styled-components' +import { th } from '@pubsweet/ui-toolkit' +import { Button, Spinner } from '@pubsweet/ui' +import { get } from 'lodash' +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 newReviewer = { + email: reviewer.email, + role: 'reviewer', + firstName: reviewer.name, + lastName: '', + } + 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/contextualBoxes/ReviewerDetails.js b/packages/component-faraday-ui/src/contextualBoxes/ReviewerDetails.js index d64a8a58c009bf3d77b62b2f5e2367b371d389c9..336bf7d1f631bcffd8b0077c9e4d7bcdfc075e03 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,12 +64,19 @@ const ReviewerDetails = ({ > <H4>Reviewer Details</H4> </TabButton> - <TabButton ml={1} mr={1} onClick={() => changeTab(1)} selected={selectedTab === 1} + > + <H4>Reviewer Suggestions</H4> + </TabButton> + <TabButton + ml={1} + mr={1} + onClick={() => changeTab(2)} + selected={selectedTab === 2} > <H4>Reviewer Reports</H4> <Tag mr={1}>{reports.length}</Tag> @@ -87,6 +99,14 @@ const ReviewerDetails = ({ </Fragment> )} {selectedTab === 1 && ( + <PublonsTable + onInvite={onInvitePublonReviewer} + publonsError={fetchingError} + publonsFetching={isFetching} + reviewers={publonReviewers} + /> + )} + {selectedTab === 2 && ( <Fragment> {reports.length === 0 && ( <Text align="center">No reports submitted yet.</Text> @@ -114,14 +134,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-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..ae1eeb0282021d4211f9048abe2fc969e5396308 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/post.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js @@ -122,9 +122,17 @@ module.exports = models => async (req, res) => { } catch (e) { const userHelper = new User({ UserModel }) + const userData = req.body + const { firstName, lastName, isPublons } = userData + + if (process.env.PUBLONS_MOCK_EMAIL && isPublons) { + const mockEmail = process.env.PUBLONS_MOCK_EMAIL + userData.email = mockEmail.replace('__NAME__', `${firstName}.${lastName}`) + } + const newUser = await userHelper.createUser({ role, - body: req.body, + body: userData, }) if (collection.status === 'heAssigned') diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index fc006093cb6812da5384997a07d97dba965ed1f3..f3bfdbb744fc13812e5afdf256cfd53c34584276 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -45,6 +45,7 @@ const ManuscriptLayout = ({ fragment = {}, changeForm, isFetching, + fetchingError, formValues, heExpanded, onHEResponse, @@ -57,18 +58,19 @@ const ManuscriptLayout = ({ onRevokeReviewerInvite, toggleReviewerResponse, invitationsWithReviewers, + publonReviewers, reviewerResponseExpanded, pendingOwnRecommendation, toggleReviewerRecommendations, reviewerRecommendationExpanded, shouldReview, submittedOwnRecommendation, - heAccepted, reviewerReports, onEditorialRecommendation, reviewerRecommendations, toggleReviewerDetails, reviewerDetailsExpanded, + onInvitePublonReviewer, }) => ( <Root pb={30}> {!isEmpty(collection) && !isEmpty(fragment) ? ( @@ -166,15 +168,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 ac4868da0b46a6367c6241e128a699d413fb9f63..af451b26a93a5d81891cb85f584e108fb404ac36 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -56,10 +56,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 +83,8 @@ export default compose( setDisplayName('ManuscriptPage'), withJournal, withRouter, + withFetching, + withState('publonReviewers', 'setPub', []), withState('editorInChief', 'setEiC', 'N/A'), ConnectPage(({ match }) => [ actions.getCollection({ id: match.params.project }), @@ -136,6 +147,7 @@ export default compose( pendingHEInvitation, pendingOwnRecommendation, pendingReviewerInvitation, + isFetching, }, ) => ({ currentUser: { @@ -165,6 +177,7 @@ export default compose( }, isFetching: { editorsFetching: selectFetching(state), + publonsFetching: isFetching, }, formValues: { eicDecision: getFormValues('eic-decision')(state), @@ -209,6 +222,8 @@ export default compose( setEiC(`${firstName} ${lastName}`) } }, + setPublons: ({ setPub }) => (publonReviewers = []) => + setPub(publonReviewers), assignHE: ({ assignHandlingEditor, fetchUpdatedCollection, @@ -403,6 +418,41 @@ export default compose( }) }, }), + withHandlers({ + onInvitePublonReviewer: ({ + collection, + fragment, + fetchUpdatedCollection, + setPublons, + setFetching: setListFetching, + setError, + clearError, + }) => (values, { hideModal, setModalError, setFetching }) => { + setFetching(true) + inviteReviewer({ + reviewerData: values, + fragmentId: fragment.id, + collectionId: collection.id, + isPublon: true, + }) + .then(() => { + setFetching(false) + hideModal() + fetchUpdatedCollection() + getPublonsReviewers({ + fragmentId: fragment.id, + setPublons, + setFetching: setListFetching, + setError, + clearError, + }) + }) + .catch(err => { + setFetching(false) + handleError(setModalError)(err) + }) + }, + }), fromRenderProps(RemoteOpener, ({ toggle, expanded }) => ({ toggleAssignHE: toggle, heExpanded: expanded, @@ -435,14 +485,24 @@ export default compose( match, history, location, + setPublons, shouldReview, reviewerReports, setEditorInChief, clearCustomError, hasManuscriptFailure, fetchUpdatedCollection, - currentUser: { isInvitedHE, isInvitedToReview, isHEToManuscript }, + currentUser: { + isInvitedHE, + isInvitedToReview, + isHEToManuscript, + permissions: { canInviteReviewers }, + }, + setFetching, + setError, + clearError, } = this.props + if (hasManuscriptFailure) { history.push('/not-found') clearCustomError() @@ -462,6 +522,16 @@ export default compose( setEditorInChief(head(res.users)), ) + if (canInviteReviewers) { + getPublonsReviewers({ + fragmentId, + setPublons, + setFetching, + setError, + clearError, + }) + } + if (isInvitedHE) { this.props.toggleHEResponse() } diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js index 031879a3af200d911cfa905707d35b3406b41395..3fd58fbc9af7b3c79394b81f3cfde6162c2d09a3 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, @@ -303,3 +304,24 @@ export const parseEicDecision = ({ decision, message }) => ({ }, ], }) + +// handle publons +export const getPublonsAPI = fragmentId => + apiGet(`/fragments/${fragmentId}/publons`) + +export const getPublonsReviewers = ({ + fragmentId, + setPublons, + setFetching, + setError, + clearError, +}) => { + clearError() + setFetching(true) + getPublonsAPI(fragmentId) + .then(res => { + setPublons(res) + setFetching(false) + }) + .catch(handleError(setError)) +} diff --git a/packages/components-faraday/src/redux/reviewers.js b/packages/components-faraday/src/redux/reviewers.js index 7542b980da4cdb1aaab21256f0b85bca2cabfb3d..9d2ee9b6333822535652d83f30b1802a21a879df 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 = ({ + reviewerData, + collectionId, + fragmentId, + isPublons = false, +}) => create(`/collections/${collectionId}/fragments/${fragmentId}/invitations`, { ...reviewerData, role: 'reviewer', + isPublons, }) // #region Actions - invitations