diff --git a/packages/component-invite/src/routes/collectionsInvitations/get.js b/packages/component-invite/src/routes/collectionsInvitations/get.js index d40c94ed1adefb39b7f7435d65c35dce996fbf25..308a57670a9030a82833f445fef7855083077edb 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/get.js +++ b/packages/component-invite/src/routes/collectionsInvitations/get.js @@ -56,6 +56,7 @@ module.exports = models => async (req, res) => { respondedOn, email: user.email, status, + userId: user.id, invitationId: id, } }) diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index 312a61ad8d4d862baf927c2b0abee1405d8dc623..24328c05a5e872612ed77b6cf44548964483a631 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -1,4 +1,5 @@ -import React from 'react' +import React, { Fragment } from 'react' +import { isEmpty } from 'lodash' import ManuscriptHeader from './ManuscriptHeader' import ManuscriptVersion from './ManuscriptVersion' @@ -20,8 +21,8 @@ import { } from './' const ManuscriptLayout = ({ - project, - version, + project = {}, + version = {}, journal, history, currentUser, @@ -30,40 +31,50 @@ const ManuscriptLayout = ({ updateManuscript, }) => ( <Root> - <Container flex={3}> - <Header> - <LeftDetails> - <BreadCrumbs> - <span onClick={() => history.push('/')}>Dashboard</span> - <span>Manuscript Details</span> - </BreadCrumbs> - <ManuscriptId>{`- ID ${project.customId}`}</ManuscriptId> - </LeftDetails> - <RightDetails> - <ManuscriptVersion project={project} /> - </RightDetails> - </Header> - <ManuscriptHeader journal={journal} project={project} version={version} /> - <ManuscriptDetails collection={project} fragment={version} /> - <ReviewsAndReports - currentUserIs={currentUserIs} - project={project} - version={version} - /> - </Container> - <SideBar flex={1}> - <SideBarActions - currentUserIs={currentUserIs} - project={project} - version={version} - /> - <SideBarRoles - currentUser={currentUser} - editorInChief={editorInChief} - project={project} - version={version} - /> - </SideBar> + {!isEmpty(project) && !isEmpty(version) ? ( + <Fragment> + <Container flex={3}> + <Header> + <LeftDetails> + <BreadCrumbs> + <span onClick={() => history.push('/')}>Dashboard</span> + <span>Manuscript Details</span> + </BreadCrumbs> + <ManuscriptId>{`- ID ${project.customId}`}</ManuscriptId> + </LeftDetails> + <RightDetails> + <ManuscriptVersion project={project} /> + </RightDetails> + </Header> + <ManuscriptHeader + journal={journal} + project={project} + version={version} + /> + <ManuscriptDetails collection={project} fragment={version} /> + <ReviewsAndReports + currentUserIs={currentUserIs} + project={project} + version={version} + /> + </Container> + <SideBar flex={1}> + <SideBarActions + currentUserIs={currentUserIs} + project={project} + version={version} + /> + <SideBarRoles + currentUser={currentUser} + editorInChief={editorInChief} + project={project} + version={version} + /> + </SideBar> + </Fragment> + ) : ( + <div>Loading...</div> + )} </Root> ) diff --git a/packages/component-manuscript/src/components/ManuscriptPage.js b/packages/component-manuscript/src/components/ManuscriptPage.js index 61760e51a03f6a48854781d3dd509e0a2201ebea..423059a5c4eb5ac5d6f3a194138524e8124024fc 100644 --- a/packages/component-manuscript/src/components/ManuscriptPage.js +++ b/packages/component-manuscript/src/components/ManuscriptPage.js @@ -12,8 +12,8 @@ import { } from 'xpub-selectors' import { get as apiGet } from 'pubsweet-client/src/helpers/api' import { compose, lifecycle, withHandlers, withState } from 'recompose' -import { reviewerDecision } from 'pubsweet-components-faraday/src/redux/reviewers' import { getSignedUrl } from 'pubsweet-components-faraday/src/redux/files' +import { reviewerDecision } from 'pubsweet-components-faraday/src/redux/reviewers' import { getHandlingEditors, selectHandlingEditors, @@ -45,6 +45,7 @@ export default compose( replace, updateVersion: actions.updateFragment, getSignedUrl, + getCollection: actions.getCollection, }, ), ConnectPage(({ currentUser, handlingEditors, project }) => { @@ -64,18 +65,6 @@ export default compose( id: version.id, ...data, }), - downloadFile: ({ getSignedUrl }) => (fileId, fileName) => e => { - e.preventDefault() - getSignedUrl(fileId).then(({ signedUrl }) => { - const a = document.createElement('a') - a.href = `${signedUrl}` - a.download = fileName - a.target = '_blank' - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - }) - }, setEditorInChief: ({ setEiC }) => eic => { if (eic) { const { firstName = '', lastName = '' } = eic @@ -87,6 +76,8 @@ export default compose( const isEic = get(currentUser, 'editorInChief') const isHe = get(currentUser, 'handlingEditor') switch (type) { + case 'isHE': + return isHe case 'staff': return isAdmin || isEic || isHe case 'adminEiC': @@ -104,14 +95,15 @@ export default compose( location, match, setEditorInChief, + getCollection, } = this.props const collectionId = match.params.project const { agree, invitationId } = parseSearchParams(location.search) if (agree === 'true') { replace(location.pathname) - reviewerDecision(invitationId, collectionId, true).catch( - redirectToError(replace), - ) + reviewerDecision(invitationId, collectionId, true) + .then(() => getCollection({ id: match.params.project })) + .catch(redirectToError(replace)) } apiGet(`/users?editorInChief=true`).then(res => diff --git a/packages/component-manuscript/src/components/ManuscriptVersion.js b/packages/component-manuscript/src/components/ManuscriptVersion.js index 2ecebdcd8a4a4804f89d632273270a9233a4e353..780b31916503239e0af3d5ed3de1313acab30d7c 100644 --- a/packages/component-manuscript/src/components/ManuscriptVersion.js +++ b/packages/component-manuscript/src/components/ManuscriptVersion.js @@ -8,20 +8,21 @@ import { withRouter } from 'react-router-dom' import { parseVersionOptions } from './utils' -const ManuscriptVersion = ({ project, fragments = [], history, match }) => ( - <Menu - inline - onChange={v => - history.push(`/projects/${project.id}/versions/${v}/details`) - } - options={parseVersionOptions(fragments)} - value={get(match, 'params.version')} - /> -) +const ManuscriptVersion = ({ project = {}, fragments = [], history, match }) => + !!fragments.length && ( + <Menu + inline + onChange={v => + history.push(`/projects/${project.id}/versions/${v}/details`) + } + options={parseVersionOptions(fragments)} + value={get(match, 'params.version')} + /> + ) export default compose( withRouter, connect((state, { project }) => ({ - fragments: selectFragments(state, project.fragments), + fragments: selectFragments(state, get(project, 'fragments') || []), })), )(ManuscriptVersion) diff --git a/packages/component-manuscript/src/components/ReviewReportCard.js b/packages/component-manuscript/src/components/ReviewReportCard.js index 077c7da4c98ab3e5ca643e18c85e89cce4b7b887..e306ed05dc681d454024cb2eda839ca0bfc0f734 100644 --- a/packages/component-manuscript/src/components/ReviewReportCard.js +++ b/packages/component-manuscript/src/components/ReviewReportCard.js @@ -1,49 +1,89 @@ import React, { Fragment } from 'react' import moment from 'moment' +import { get, isEmpty } from 'lodash' import { th } from '@pubsweet/ui' import { compose } from 'recompose' import { withJournal } from 'xpub-journal' import styled, { css } from 'styled-components' import { FileItem } from 'pubsweet-components-faraday/src/components/Files' -const ReviewReportCard = ({ report = {}, journal: { recommendations } }) => ( - <Root> - <Row> - <Label>Recommendation</Label> - {report.submittedOn && ( - <Text> - Submitted on: {moment(report.submittedOn).format('DD.MM.YYYY')} - </Text> - )} - </Row> - <Row> - <Text> - {recommendations.find(r => report.recommendation === r.value).label} - </Text> - </Row> - <Spacing /> - <Row left> - <Label>Report Text</Label> - </Row> - <Row> - <Text>{report.comments[0].content}</Text> - </Row> - <Spacing /> +import ShowMore from './ShowMore' - {!!report.comments[0].files.length && ( - <Fragment> - <Row left> - <Label>Files</Label> - </Row> - <Row left> - {report.comments[0].files.map(file => ( - <FileItem compact id={file.id} key={file.id} {...file} /> - ))} +const ReviewReportCard = ({ + i = 0, + report = {}, + journal: { recommendations }, +}) => { + const hasReviewer = !isEmpty(get(report, 'user')) + const { submittedOn, comments = [], user } = report + const submittedDate = moment(submittedOn).format('DD.MM.YYYY') + const publicComment = comments.find(c => c.public) + const privateComment = comments.find(c => !c.public) + const recommendationLabel = get( + recommendations.find(r => report.recommendation === r.value), + 'label', + ) + + return ( + <Root hasReviewer={hasReviewer}> + {hasReviewer && ( + <Row> + <Text> + <b>Reviewer {i}</b> + <Underline>{user.name}</Underline> + <span>{user.email}</span> + </Text> + {submittedDate && <Text>{submittedDate}</Text>} </Row> - </Fragment> - )} - </Root> -) + )} + <Row> + <Label>Recommendation</Label> + {submittedDate && !hasReviewer && <Text>{submittedDate}</Text>} + </Row> + <Row> + <Text>{recommendationLabel}</Text> + </Row> + {get(publicComment, 'content') && ( + <Fragment> + <Spacing /> + <Row left> + <Label>Report Text</Label> + </Row> + <Row> + <ShowMore content={publicComment.content} /> + </Row> + </Fragment> + )} + + {get(publicComment, 'files') && + !!publicComment.files.length && ( + <Fragment> + <Spacing /> + <Row left> + <Label>Files</Label> + </Row> + <Row left> + {publicComment.files.map(file => ( + <FileItem compact id={file.id} key={file.id} {...file} /> + ))} + </Row> + </Fragment> + )} + + {get(privateComment, 'content') && ( + <Fragment> + <Spacing /> + <Row left> + <Label>Confidential Note</Label> + </Row> + <Row> + <ShowMore content={privateComment.content} /> + </Row> + </Fragment> + )} + </Root> + ) +} export default compose(withJournal)(ReviewReportCard) @@ -53,16 +93,29 @@ const defaultText = css` font-family: ${th('fontReading')}; font-size: ${th('fontSizeBaseSmall')}; ` + +const cardStyle = css` + margin: 0 auto calc(${th('subGridUnit')}*3); + border: ${th('borderDefault')}; + padding: calc(${th('subGridUnit')}*2); +` + const Root = styled.div` display: flex; flex-direction: column; margin: auto; - [role='listbox'] { - min-width: 280px; - } + border: none; + padding: 0; + ${({ hasReviewer }) => (hasReviewer ? cardStyle : null)}; ` const Text = styled.div` ${defaultText}; + span { + margin-left: calc(${th('subGridUnit')}*3); + } +` +const Underline = styled.span` + text-decoration: underline; ` const Label = styled.div` ${defaultText}; @@ -88,4 +141,5 @@ const Row = styled.div` justify-content: ${({ left }) => (left ? 'left' : 'space-between')}; ${defaultText}; ` + // #endregion diff --git a/packages/component-manuscript/src/components/ReviewerReportForm.js b/packages/component-manuscript/src/components/ReviewerReportForm.js index 547c85f9fb2ddb6ac37f8a41a31ca8797d25363f..aa0cbd68be5871e2628246d0adb9e47a1658081d 100644 --- a/packages/component-manuscript/src/components/ReviewerReportForm.js +++ b/packages/component-manuscript/src/components/ReviewerReportForm.js @@ -93,12 +93,16 @@ const ReviewerReportForm = ({ <Row left> <Label>Report</Label> {!fileFetching.review ? ( - <FilePicker - allowedFileExtensions={['pdf', 'doc', 'docx']} - onUpload={addFile} - > - <ActionText left={12}>Upload file</ActionText> - </FilePicker> + <Fragment> + {isEmpty(formValues.files) && ( + <FilePicker + allowedFileExtensions={['pdf', 'doc', 'docx']} + onUpload={addFile} + > + <ActionText left={12}>Upload file</ActionText> + </FilePicker> + )} + </Fragment> ) : ( <Spinner size={2} /> )} @@ -111,10 +115,12 @@ const ReviewerReportForm = ({ {...input} hasError={input.validationStatus === 'error'} onChange={e => changeField('public', e.target.value)} + readOnly={fileFetching.review} rows={6} /> )} name="public" + readOnly={fileFetching.review} validate={isEmpty(formValues.files) ? [required] : []} /> </FullWidth> @@ -153,10 +159,12 @@ const ReviewerReportForm = ({ {...input} hasError={input.validationStatus === 'error'} onChange={e => changeField('confidential', e.target.value)} + readOnly={fileFetching.review} rows={6} /> )} name="confidential" + readOnly={fileFetching.review} validate={[required]} /> </FullWidth> @@ -235,10 +243,7 @@ export default compose( .then(file => { const files = formValues.files || [] const newFiles = [...files, file] - - setTimeout(() => { - changeForm('reviewerReport', 'files', newFiles) - }, 1000) + changeForm('reviewerReport', 'files', newFiles) }) .catch(e => console.error(`Couldn't upload file.`, e)) }, @@ -314,6 +319,10 @@ const Textarea = styled.textarea` font-family: ${th('fontWriting')}; border-color: ${({ hasError }) => hasError ? th('colorError') : th('colorPrimary')}; + transition: all 300ms linear; + &:read-only { + background-color: ${th('colorBackgroundHue')}; + } ` const Spacing = styled.div` diff --git a/packages/component-manuscript/src/components/ReviewsAndReports.js b/packages/component-manuscript/src/components/ReviewsAndReports.js index 62feede02d3d178b3b2b6384932328acd35087bc..57e783f4f083cf6984834e69a5d8c83575116002 100644 --- a/packages/component-manuscript/src/components/ReviewsAndReports.js +++ b/packages/component-manuscript/src/components/ReviewsAndReports.js @@ -19,7 +19,7 @@ import { selectRecommendations } from 'pubsweet-components-faraday/src/redux/rec import Tabs from '../molecules/Tabs' import Expandable from '../molecules/Expandable' -const getTabSections = (collectionId, reviewers) => [ +const getTabSections = (collectionId, reviewers, recommendations = []) => [ { key: 1, label: 'Reviewers Details', @@ -30,7 +30,17 @@ const getTabSections = (collectionId, reviewers) => [ { key: 2, label: 'Reviewer Reports', - content: <div>Reviewer Reports Content</div>, + content: ( + <Fragment> + {recommendations.length ? ( + recommendations.map((r, index) => ( + <ReviewReportCard i={index + 1} key={r.id} report={r} /> + )) + ) : ( + <div>No reports submitted yet.</div> + )} + </Fragment> + ), }, ] @@ -43,6 +53,8 @@ const ReviewsAndReports = ({ currentUserIs, report, review = {}, + mappedRecommendations, + mappedReviewers, }) => ( <Fragment> {currentUserIs('staff') && ( @@ -50,13 +62,21 @@ const ReviewsAndReports = ({ <Expandable label="Reviewers & Reports" rightHTML={ - <ReviewerBreakdown type="reviewer" values={reviewers || []} /> + <ReviewerBreakdown + type="reviewer" + values={reviewers || []} + versionId={version.id} + /> } startExpanded > <Tabs activeKey={1} - sections={getTabSections(project.id, reviewers)} + sections={getTabSections( + project.id, + mappedReviewers(), + mappedRecommendations(), + )} /> </Expandable> </Root> @@ -93,6 +113,16 @@ export default compose( getReviewers: ({ project, getCollectionReviewers }) => () => { getCollectionReviewers(project.id) }, + mappedRecommendations: ({ recommendations, reviewers }) => () => + recommendations.map(r => ({ + ...r, + user: reviewers.find(user => user.userId === r.userId), + })), + mappedReviewers: ({ recommendations, reviewers }) => () => + reviewers.map(r => ({ + ...r, + review: recommendations.find(rec => rec.userId === r.userId), + })), }), withProps(({ recommendations = [] }) => ({ report: head(recommendations.filter(r => r.submittedOn)), diff --git a/packages/component-manuscript/src/components/ShowMore.js b/packages/component-manuscript/src/components/ShowMore.js new file mode 100644 index 0000000000000000000000000000000000000000..df021730b5074b2714b63f0d866a750556da94a4 --- /dev/null +++ b/packages/component-manuscript/src/components/ShowMore.js @@ -0,0 +1,89 @@ +import React, { Fragment } from 'react' +import { Icon } from '@pubsweet/ui' +import styled from 'styled-components' + +const ShowMore = ({ content = '', words = 50 }) => { + const contentSize = content.split(' ').length + return ( + <Root> + {contentSize > words && ( + <Fragment> + <Input id="read-more-target" role="button" type="checkbox" /> + <Control htmlFor="read-more-target"> + <div> + <Icon primary>chevron-down</Icon> Show All + </div> + <div> + <Icon primary>chevron-up</Icon> Show Less + </div> + </Control> + </Fragment> + )} + <Container bigger={contentSize > words}>{content}</Container> + </Root> + ) +} + +export default ShowMore + +// #region styled-components +const Root = styled.div` + display: flex; + flex-direction: column; +` +const Container = styled.section` + height: ${({ bigger }) => (bigger ? '100px' : 'initial')}; + overflow: hidden; + order: -1; +` + +const Input = styled.input` + border: 0; + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + + &:checked { + ~ section { + height: initial; + overflow: unset; + } + ~ label { + div:first-of-type { + display: none; + } + div:last-of-type { + display: inline-block; + } + } + } + ~ label { + div:first-of-type { + display: inline-block; + } + div:last-of-type { + display: none; + } + } +` + +const Control = styled.label` + cursor: pointer; + div { + text-decoration: underline; + text-transform: uppercase; + > span { + vertical-align: sub; + padding-left: 0; + } + } + &:hover { + opacity: 0.7; + } +` + +// #endregion diff --git a/packages/component-manuscript/src/components/SideBarActions.js b/packages/component-manuscript/src/components/SideBarActions.js index e31d92a43e2cb4ac057523a11b2afc4fbc9277e8..ba0da4b513a84efbc2e01a4f432cfb050d5719e5 100644 --- a/packages/component-manuscript/src/components/SideBarActions.js +++ b/packages/component-manuscript/src/components/SideBarActions.js @@ -1,13 +1,21 @@ import React from 'react' import { th, Icon } from '@pubsweet/ui' import styled from 'styled-components' - import ZipFiles from 'pubsweet-components-faraday/src/components/Files/ZipFiles' +import { Recommendation } from 'pubsweet-components-faraday/src/components/MakeRecommendation' + import { MakeDecision } from './' const SideBarActions = ({ project, version, currentUserIs }) => ( <Root> - {currentUserIs('adminEiC') ? <MakeDecision /> : <div />} + {currentUserIs('adminEiC') && <MakeDecision />} + {currentUserIs('isHE') && ( + <Recommendation + collectionId={project.id} + fragmentId={version.id} + modalKey={`recommend-${version.id}`} + /> + )} <ZipFiles archiveName={`ID-${project.customId}`} collectionId={project.id} diff --git a/packages/component-manuscript/src/components/utils.js b/packages/component-manuscript/src/components/utils.js index 0ffd247ad00e55e06063eba4a0919b95ab582e2e..6ef3a6061c0a96982331d975850f958f9d136e9f 100644 --- a/packages/component-manuscript/src/components/utils.js +++ b/packages/component-manuscript/src/components/utils.js @@ -69,7 +69,7 @@ export const parseSearchParams = url => { return parsedObject } -export const parseVersionOptions = fragments => +export const parseVersionOptions = (fragments = []) => fragments.map(f => ({ value: f.id, label: `Version ${f.version} - updated on ${moment(f.submitted).format( diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index e2456344997492259fe08b99369f8d5f30f94ace..a177280e3c4146808fac5898d35903c4db356d7d 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -14,24 +14,27 @@ import AuthorsWithTooltip from 'pubsweet-component-manuscript/src/molecules/Auth import ZipFiles from '../Files/ZipFiles' import { InviteReviewers } from '../Reviewers/' +import { currentUserIs } from '../../redux/users' import { selectInvitation } from '../../redux/reviewers' -import { parseVersion, parseJournalIssue, mapStatusToLabel } from './../utils' import { ReviewerDecision, HandlingEditorSection } from './' +import { parseVersion, parseJournalIssue, mapStatusToLabel } from './../utils' import { ReviewerBreakdown } from '../Invitations' +import { Recommendation } from '../MakeRecommendation' const DashboardCard = ({ - deleteProject, + isHE, + theme, + journal, history, project, version, - showAbstractModal, - journal, - showConfirmationModal, - theme, + invitation, currentUser, + deleteProject, + showAbstractModal, canInviteReviewers, - invitation, + showConfirmationModal, ...rest }) => { const { submitted, title, type } = parseVersion(version) @@ -57,6 +60,13 @@ const DashboardCard = ({ /> </LeftDetails> <RightDetails flex={2}> + {isHE && ( + <Recommendation + collectionId={project.id} + fragmentId={version.id} + modalKey={`recommend-${version.id}`} + /> + )} <ZipFiles archiveName={`ID-${project.customId}`} collectionId={project.id} @@ -131,6 +141,7 @@ const DashboardCard = ({ <InviteReviewers modalKey={`invite-reviewers-${project.id}`} project={project} + version={version} /> </RightDetails> )} @@ -159,6 +170,7 @@ export default compose( modalComponent: ConfirmationModal, }), connect((state, { project }) => ({ + isHE: currentUserIs(state, 'handlingEditor'), invitation: selectInvitation(state, project.id), })), withHandlers({ diff --git a/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js b/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js index aebc1000d766066fc1648a6a1f7df2e75c1387cb..0e62a38363dd0ebdf6fecad2261cdd379f5ad1b4 100644 --- a/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js +++ b/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js @@ -1,5 +1,7 @@ import React from 'react' import { th } from '@pubsweet/ui' +import { connect } from 'react-redux' +import { selectFragment } from 'xpub-selectors' import styled, { css } from 'styled-components' import { compose, withHandlers, withProps } from 'recompose' @@ -16,11 +18,14 @@ const BREAKDOWN_TYPES = { const reviewFilter = r => r.status === 'accepted' const invitationFilter = i => i.hasAnswer && i.isAccepted +const submittedFilter = r => + r.status === 'accepted' && r.review && r.review.submittedOn const roleFilter = role => i => i.role === role const reviewerReduce = (acc, r) => ({ ...acc, [r.status]: acc[r.status] + 1, + submitted: submittedFilter(r) ? acc.submitted + 1 : acc.submitted, }) const invitationReduce = (acc, i) => { @@ -35,6 +40,9 @@ const invitationReduce = (acc, i) => { } export default compose( + connect((state, { versionId }) => ({ + fragment: selectFragment(state, versionId), + })), withProps(({ values = [], type = BREAKDOWN_TYPES.invitation }) => ({ values: type === BREAKDOWN_TYPES.invitation @@ -57,13 +65,21 @@ export default compose( }, getExtendedReport: ({ values = [], + fragment = {}, + versionId, type = BREAKDOWN_TYPES.invitation, }) => () => { - const report = values.reduce( + const { recommendations = [] } = fragment + const mappedValues = values.map(r => ({ + ...r, + review: recommendations.find(rec => rec.userId === r.userId), + })) + const report = mappedValues.reduce( type === BREAKDOWN_TYPES.invitation ? invitationReduce : reviewerReduce, { accepted: 0, declined: 0, + submitted: 0, }, ) @@ -72,6 +88,7 @@ export default compose( <b>{values.length}</b> invited, <b> {report.accepted}</b> agreed, <b> {report.declined}</b> declined + <b> {report.submitted}</b> submitted </BreakdownText> ) }, diff --git a/packages/components-faraday/src/components/MakeRecommendation/FormItems.js b/packages/components-faraday/src/components/MakeRecommendation/FormItems.js new file mode 100644 index 0000000000000000000000000000000000000000..66534ec0c5d026dc771fefd50fa05596c74d05d6 --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/FormItems.js @@ -0,0 +1,80 @@ +import { th } from '@pubsweet/ui' +import styled from 'styled-components' + +export const RootContainer = styled.div` + background-color: ${th('backgroundColorReverse')}; + border: ${({ bordered }) => (bordered ? th('borderDefault') : 'none')}; + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: 550px; + min-width: 450px; + padding: calc(${th('subGridUnit')} * 2) calc(${th('subGridUnit')} * 4); +` + +export const Title = styled.div` + color: ${th('colorPrimary')}; + font-family: ${th('fontHeading')}; + font-size: ${th('fontSizeHeading5')}; + font-weight: bold; + margin: 10px auto; + text-align: center; +` +export const Subtitle = styled.div` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBase')}; + font-weight: normal; + margin: 10px auto; + text-align: center; +` + +export const Email = styled.div` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBase')}; + font-weight: normal; + margin: 10px auto; + text-align: center; +` + +export const FormContainer = styled.form`` + +export const Row = styled.div` + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-evenly; + margin: calc(${th('subGridUnit')} * 2) 0; +` + +export const RowItem = styled.div` + display: flex; + flex: ${({ flex }) => flex || 1}; + flex-direction: ${({ vertical }) => (vertical ? 'column' : 'row')}; + justify-content: ${({ centered }) => (centered ? 'center' : 'initial')}; +` + +export const Label = styled.div` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; + text-transform: uppercase; +` +export const Err = styled.span` + color: ${th('colorError')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBase')}; + margin-top: calc(${th('gridUnit')}*-1); + text-align: center; +` + +export const Textarea = styled.textarea` + width: 400px; + padding: calc(${th('subGridUnit')}*2); + font-size: ${th('fontSizeBaseSmall')}; + font-family: ${th('fontWriting')}; + border-color: ${({ hasError }) => + hasError ? th('colorError') : th('colorPrimary')}; + transition: all 300ms linear; + &:read-only { + background-color: ${th('colorBackgroundHue')}; + } +` diff --git a/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js new file mode 100644 index 0000000000000000000000000000000000000000..3519e1a5eb6371d2f5517b49fac44dfa0c3bcd7c --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/RecommendWizard.js @@ -0,0 +1,96 @@ +import React from 'react' +import { get } from 'lodash' +import { Icon } from '@pubsweet/ui' +import { connect } from 'react-redux' +import styled from 'styled-components' +import { getFormValues, reset as resetForm } from 'redux-form' +import { compose, withState, withHandlers } from 'recompose' + +import { StepOne, StepTwo, RootContainer } from './' +import { + selectError, + selectFetching, + createRecommendation, +} from '../../redux/recommendations' + +const RecommendWizard = ({ + step, + decision, + nextStep, + prevStep, + hideModal, + submitForm, + isFetching, + recommendationError, + ...rest +}) => ( + <RootContainer> + <IconButton onClick={hideModal}> + <Icon primary>x</Icon> + </IconButton> + {step === 0 && ( + <StepOne disabled={!decision} onSubmit={nextStep} {...rest} /> + )} + {step === 1 && ( + <StepTwo + decision={decision} + goBack={prevStep} + isFetching={isFetching} + onSubmit={submitForm} + recommendationError={recommendationError} + /> + )} + </RootContainer> +) + +export default compose( + connect( + state => ({ + isFetching: selectFetching(state), + recommendationError: selectError(state), + decision: get(getFormValues('recommendation')(state), 'decision'), + }), + { + createRecommendation, + resetForm, + }, + ), + withState('step', 'changeStep', 0), + withHandlers({ + nextStep: ({ changeStep }) => () => changeStep(s => s + 1), + prevStep: ({ changeStep }) => () => changeStep(s => (s === 0 ? 0 : s - 1)), + submitForm: ({ + showModal, + hideModal, + resetForm, + fragmentId, + collectionId, + createRecommendation, + }) => values => { + const recommendation = { + recommendation: values.decision, + recommendationType: 'editorRecommendation', + } + if (values.message) { + recommendation.comments = Object.values(values.message).map(m => ({ + content: m, + public: true, + })) + } + createRecommendation(collectionId, fragmentId, recommendation).then(r => { + resetForm('recommendation') + showModal({ + title: 'Recommendation sent', + }) + }) + }, + }), +)(RecommendWizard) + +// #region styled components +const IconButton = styled.div` + align-self: flex-end; + cursor: pointer; +` + +// #endregion diff --git a/packages/components-faraday/src/components/MakeRecommendation/Recommendation.js b/packages/components-faraday/src/components/MakeRecommendation/Recommendation.js new file mode 100644 index 0000000000000000000000000000000000000000..004eb9ebe53b03c89fbe5d46359ec70868f5f611 --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/Recommendation.js @@ -0,0 +1,57 @@ +import React from 'react' +import { th } from '@pubsweet/ui' +import styled from 'styled-components' +import { compose, withHandlers } from 'recompose' + +import { + ConfirmationModal, + withModal2, +} from 'pubsweet-component-modal/src/components' +import { RecommendWizard } from './' + +const Recommendation = ({ showFirstStep }) => ( + <Root onClick={showFirstStep}>Make recommendation</Root> +) + +const SHOW_WIZARD = 'SHOW_WIZARD' + +const ModalComponent = ({ type, ...rest }) => { + switch (type) { + case SHOW_WIZARD: + return <RecommendWizard {...rest} /> + default: + return <ConfirmationModal {...rest} /> + } +} + +export default compose( + withModal2(props => ({ + modalComponent: ModalComponent, + })), + withHandlers({ + showFirstStep: ({ collectionId, fragmentId, showModal }) => () => { + showModal({ + type: SHOW_WIZARD, + collectionId, + fragmentId, + }) + }, + }), +)(Recommendation) + +// #region styled components +const Root = styled.div` + align-items: center; + background-color: ${th('colorPrimary')}; + color: ${th('colorTextReverse')}; + cursor: pointer; + display: flex; + font-family: ${th('fontInterface')}; + font-size: ${th('fontSizeBaseSmall')}; + height: calc(${th('subGridUnit')} * 5); + justify-content: center; + min-width: 200px; + padding: 0 calc(${th('subGridUnit')} * 2); + text-transform: uppercase; +` +// #endregion diff --git a/packages/components-faraday/src/components/MakeRecommendation/StepOne.js b/packages/components-faraday/src/components/MakeRecommendation/StepOne.js new file mode 100644 index 0000000000000000000000000000000000000000..5ce88ae43199a61e31aa07daa504e2bc1c6fb5dd --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/StepOne.js @@ -0,0 +1,44 @@ +import React from 'react' +import { reduxForm } from 'redux-form' +import { RadioGroup, ValidatedField, Button } from '@pubsweet/ui' + +import { RootContainer, Row, RowItem, Title } from './' + +const radioOptions = [ + { value: 'reject', label: 'Reject' }, + { value: 'publish', label: 'Publish' }, + { value: 'revise', label: 'Request revision' }, +] + +const StepOne = ({ hideModal, disabled, onSubmit }) => ( + <RootContainer> + <Title>Recommendation for Next Phase</Title> + <Row> + <RowItem> + <ValidatedField + component={input => ( + <RadioGroup name="decision" options={radioOptions} {...input} /> + )} + name="decision" + /> + </RowItem> + </Row> + <Row> + <RowItem centered> + <Button onClick={hideModal}>Cancel</Button> + </RowItem> + <RowItem centered> + <Button disabled={disabled} onClick={onSubmit} primary> + Next + </Button> + </RowItem> + </Row> + </RootContainer> +) + +export default reduxForm({ + form: 'recommendation', + destroyOnUnmount: false, + enableReinitialize: false, + forceUnregisterOnUnmount: true, +})(StepOne) diff --git a/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js b/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js new file mode 100644 index 0000000000000000000000000000000000000000..1f7f86db265fb432e5bed61a5a7f0411ee44827a --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/StepTwo.js @@ -0,0 +1,209 @@ +import React, { Fragment } from 'react' +import { capitalize } from 'lodash' +import { connect } from 'react-redux' +import { required } from 'xpub-validators' +import styled, { css } from 'styled-components' +import { reduxForm, change as changeForm } from 'redux-form' +import { compose, withState, withHandlers } from 'recompose' +import { + th, + Icon, + Button, + Spinner, + RadioGroup, + ValidatedField, +} from '@pubsweet/ui' + +import { + Row, + Err, + Label, + Title, + RowItem, + Textarea, + RootContainer, + FormContainer, +} from './' + +const revisionOptions = [ + { value: 'minor', label: 'Minor' }, + { value: 'major', label: 'Major' }, +] + +const Form = RootContainer.withComponent(FormContainer) + +const StepTwo = ({ + goBack, + hasNote, + decision, + showNote, + removeNote, + isFetching, + handleSubmit, + recommendationError, +}) => ( + <Form onSubmit={handleSubmit}> + <Title> + {decision !== 'revise' + ? `Recommandation to ${capitalize(decision)}` + : `Request a revision from Author`} + </Title> + {decision !== 'revise' ? ( + <Fragment> + <Row> + <RowItem vertical> + <Label>Message for Editor in Chief (optional)</Label> + <ValidatedField + component={input => <Textarea {...input} />} + name="message.eic" + /> + </RowItem> + </Row> + <Row> + <RowItem vertical> + <Label>Message for Author (optional)</Label> + <ValidatedField + component={input => <Textarea {...input} />} + name="message.author" + /> + </RowItem> + </Row> + {recommendationError && ( + <Row> + <RowItem centered> + <Err>{recommendationError}</Err> + </RowItem> + </Row> + )} + </Fragment> + ) : ( + <Fragment> + <CustomRow> + <RowItem vertical> + <Label>REVISION TYPE</Label> + <ValidatedField + component={input => ( + <RadioGroup + name="revision.revision-type" + {...input} + options={revisionOptions} + /> + )} + name="revision.revisionType" + /> + </RowItem> + </CustomRow> + <CustomRow> + <RowItem vertical> + <Label>REASON & DETAILS</Label> + <ValidatedField + component={input => <Textarea {...input} />} + name="revision.reason" + validate={[required]} + /> + </RowItem> + </CustomRow> + {!hasNote ? ( + <Row> + <RowItem> + <TextButton onClick={showNote}>Add Internal Note</TextButton> + <HintText>Not shared with author</HintText> + </RowItem> + </Row> + ) : ( + <Fragment> + <CustomRow> + <RowItem> + <Label>INTERNAL NOTE</Label> + </RowItem> + <RowItem> + <HintText>Not shared with author</HintText> + </RowItem> + <CustomRowItem onClick={removeNote}> + <IconButton> + <Icon primary>x</Icon> + </IconButton> + <TextButton>Remove</TextButton> + </CustomRowItem> + </CustomRow> + <CustomRow> + <RowItem> + <ValidatedField + component={input => <Textarea {...input} />} + name="revision.internal-note" + /> + </RowItem> + </CustomRow> + </Fragment> + )} + </Fragment> + )} + <Row> + <RowItem centered> + <Button onClick={goBack}>Back</Button> + </RowItem> + <RowItem centered> + {isFetching ? ( + <Spinner size={3} /> + ) : ( + <Button primary type="submit"> + Submit + </Button> + )} + </RowItem> + </Row> + </Form> +) + +export default compose( + connect(null, { changeForm }), + withState('hasNote', 'changeHasNote', false), + withHandlers({ + showNote: ({ changeHasNote }) => () => { + changeHasNote(true) + }, + removeNote: ({ changeHasNote, changeForm }) => () => { + changeHasNote(false) + changeForm('recommendation', 'revision.internal-note', '') + }, + }), + reduxForm({ + form: 'recommendation', + destroyOnUnmount: false, + forceUnregisterOnUnmount: true, + }), +)(StepTwo) + +// #region styled components +const defaultText = css` + color: ${th('colorPrimary')}; + font-family: ${th('fontInterface')}; + font-size: ${th('fontSizeBaseSmall')}; +` + +const TextButton = styled.span` + ${defaultText}; + cursor: pointer; + margin-right: ${th('subGridUnit')}; + text-decoration: underline; +` + +const HintText = styled.span` + ${defaultText}; + font-style: oblique; +` + +const IconButton = styled.div` + display: flex; + justify-content: center; +` + +const CustomRowItem = RowItem.extend` + align-items: center; + justify-content: flex-end; +` + +const CustomRow = Row.extend` + margin: ${th('subGridUnit')} 0; +` +// #endregion diff --git a/packages/components-faraday/src/components/MakeRecommendation/index.js b/packages/components-faraday/src/components/MakeRecommendation/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a8545992fa0500b61b139077740ab9b9a54e2431 --- /dev/null +++ b/packages/components-faraday/src/components/MakeRecommendation/index.js @@ -0,0 +1,5 @@ +export * from './FormItems' +export { default as StepOne } from './StepOne' +export { default as StepTwo } from './StepTwo' +export { default as Recommendation } from './Recommendation' +export { default as RecommendWizard } from './RecommendWizard' diff --git a/packages/components-faraday/src/components/Reviewers/InviteReviewers.js b/packages/components-faraday/src/components/Reviewers/InviteReviewers.js index bd6dc66d48f04bfe823b91985e52472e79840fd9..1e857cd94c8e955c7dba75d59562e2ecd6760001 100644 --- a/packages/components-faraday/src/components/Reviewers/InviteReviewers.js +++ b/packages/components-faraday/src/components/Reviewers/InviteReviewers.js @@ -55,8 +55,9 @@ const InviteReviewersModal = compose( hideModal, onConfirm, showModal, - reviewers, - invitations, + reviewers = [], + invitations = [], + versionId, collectionId, getReviewers, reviewerError, @@ -83,7 +84,11 @@ const InviteReviewersModal = compose( {reviewers.length > 0 && ( <Fragment> <Subtitle>Reviewers Info</Subtitle> - <ReviewerBreakdown type="review" values={reviewers} /> + <ReviewerBreakdown + type="review" + values={reviewers} + versionId={versionId} + /> </Fragment> )} {fetchingReviewers && <Spinner size={3} />} @@ -92,6 +97,7 @@ const InviteReviewersModal = compose( collectionId={collectionId} reviewers={reviewers} showModal={showModal} + versionId={versionId} /> </Root> ), @@ -118,6 +124,7 @@ export default compose( withHandlers({ showInviteModal: ({ project, + version, hideModal, showModal, clearReviewersError, @@ -126,6 +133,7 @@ export default compose( showModal({ type: 'invite-reviewers', collectionId: project.id, + versionId: version.id, invitations: project.invitations, onConfirm: () => { hideModal() diff --git a/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js b/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js index 9fba420117bb6cec6b81ebf7e1866a91f1df501d..a3df9a1caa6c9d33165cf864b2d3912f1538830f 100644 --- a/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js +++ b/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js @@ -1,6 +1,6 @@ import React from 'react' import moment from 'moment' -import { pick } from 'lodash' +import { pick, get } from 'lodash' import { connect } from 'react-redux' import { th, Icon } from '@pubsweet/ui' import styled, { withTheme } from 'styled-components' @@ -40,33 +40,36 @@ const TR = ({ renderTimestamp, showConfirmResend, showConfirmRevoke, -}) => ( - <Row> - <td> - <ReviewerName>{r.name}</ReviewerName>{' '} - {r.status === 'accepted' && ( - <AcceptedReviewer>{renderAcceptedLabel(index)}</AcceptedReviewer> - )} - </td> - <td>{r.invitedOn ? renderTimestamp(r.invitedOn) : ''}</td> - <td> - <StatusText>{r.status === 'accepted' ? 'Agreed' : r.status}</StatusText> - <DateText> - {r.respondedOn ? `: ${renderTimestamp(r.respondedOn)}` : ''} - </DateText> - </td> - <td> {r.submittedOn ? `: ${renderTimestamp(r.submittedOn)}` : ''} </td> - <td width={100}> - {r.status === 'pending' && ( - <ResendRevoke - showConfirmResend={showConfirmResend(r)} - showConfirmRevoke={showConfirmRevoke(r.invitationId)} - status={r.status} - /> - )} - </td> - </Row> -) +}) => { + const submittedOn = get(r, 'review.submittedOn') + return ( + <Row> + <td> + <ReviewerName>{r.name}</ReviewerName>{' '} + {r.status === 'accepted' && ( + <AcceptedReviewer>{renderAcceptedLabel(index)}</AcceptedReviewer> + )} + </td> + <td>{r.invitedOn ? renderTimestamp(r.invitedOn) : ''}</td> + <td> + <StatusText>{r.status === 'accepted' ? 'Agreed' : r.status}</StatusText> + <DateText> + {r.respondedOn ? `: ${renderTimestamp(r.respondedOn)}` : ''} + </DateText> + </td> + <td>{submittedOn ? `${renderTimestamp(submittedOn)}` : ''}</td> + <td width={100}> + {r.status === 'pending' && ( + <ResendRevoke + showConfirmResend={showConfirmResend(r)} + showConfirmRevoke={showConfirmRevoke(r.invitationId)} + status={r.status} + /> + )} + </td> + </Row> + ) +} const ReviewersDetailsList = ({ renderAcceptedLabel, reviewers, diff --git a/packages/components-faraday/src/redux/recommendations.js b/packages/components-faraday/src/redux/recommendations.js index a86bfc4758accc0282602783c5927db65940cd0b..067386a832b90a19b2487ab1d5eaff8678438a17 100644 --- a/packages/components-faraday/src/redux/recommendations.js +++ b/packages/components-faraday/src/redux/recommendations.js @@ -61,6 +61,7 @@ export const createRecommendation = ( const errorMessage = get(JSON.parse(error), 'error') dispatch(recommendationsError(errorMessage)) } + throw err }, ) } diff --git a/packages/components-faraday/src/redux/users.js b/packages/components-faraday/src/redux/users.js new file mode 100644 index 0000000000000000000000000000000000000000..dd9cd57ab2db907fbbeb5bd31a46a90afb859c06 --- /dev/null +++ b/packages/components-faraday/src/redux/users.js @@ -0,0 +1,4 @@ +import { get } from 'lodash' + +export const currentUserIs = (state, role) => + get(state, `currentUser.user.${role}`) diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index fec2ea253b827473fa580f507038727af5c95f9b..526f07f749a39fd5a713c9d8f34a82226ba9e21f 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -92,7 +92,9 @@ module.exports = { Joi.object({ id: Joi.string().required(), userId: Joi.string().required(), - recommendationType: Joi.string().required(), + recommendationType: Joi.string() + .valid(['review', 'editorRecommendation']) + .required(), submittedOn: Joi.date(), createdOn: Joi.date(), updatedOn: Joi.date(),