diff --git a/packages/component-manuscript/src/components/ReviewsAndReports.js b/packages/component-manuscript/src/components/ReviewsAndReports.js index d8e393338d1d728023ed2ce612131b109f08c9c8..1a5e9c555ad69cdc99c0f49d93254508ee455a9f 100644 --- a/packages/component-manuscript/src/components/ReviewsAndReports.js +++ b/packages/component-manuscript/src/components/ReviewsAndReports.js @@ -1,15 +1,27 @@ import React from 'react' import { th } from '@pubsweet/ui' +import { connect } from 'react-redux' import styled from 'styled-components' +import { compose, withHandlers, lifecycle } from 'recompose' + +import { ReviewerBreakdown } from 'pubsweet-components-faraday/src/components/Invitations' +import { ReviewersList } from 'pubsweet-components-faraday/src/components/Reviewers' +import { + selectReviewers, + selectFetchingReviewers, + getCollectionReviewers, +} from 'pubsweet-components-faraday/src/redux/reviewers' import Tabs from '../molecules/Tabs' import Expandable from '../molecules/Expandable' -const tabSections = [ +const getTabSections = (collectionId, reviewers) => [ { key: 1, label: 'Reviewers Details', - content: <div>Reviewers Details Content</div>, + content: ( + <ReviewersList collectionId={collectionId} reviewers={reviewers} /> + ), }, { key: 2, @@ -18,15 +30,38 @@ const tabSections = [ }, ] -const ReviewsAndReports = () => ( +const ReviewsAndReports = ({ project, reviewers = [] }) => ( <Root> - <Expandable label="Reviewers & Reports" startExpanded> - <Tabs activeKey={1} sections={tabSections} /> + <Expandable + label="Reviewers & Reports" + rightHTML={<ReviewerBreakdown values={project.invitations || []} />} + startExpanded + > + <Tabs activeKey={1} sections={getTabSections(project.id, reviewers)} /> </Expandable> </Root> ) -export default ReviewsAndReports +export default compose( + connect( + state => ({ + reviewers: selectReviewers(state), + fetchingReviewers: selectFetchingReviewers(state), + }), + { getCollectionReviewers }, + ), + withHandlers({ + getReviewers: ({ project, setReviewers, getCollectionReviewers }) => () => { + getCollectionReviewers(project.id) + }, + }), + lifecycle({ + componentDidMount() { + const { getReviewers } = this.props + getReviewers() + }, + }), +)(ReviewsAndReports) // #region styled-components diff --git a/packages/component-manuscript/src/molecules/Expandable.js b/packages/component-manuscript/src/molecules/Expandable.js index 4eeb356fcdd39efb7bfe411143d1cb6ff9bca297..b12ab8fbc552f55f9e78ce3a4af2e78e068d9baa 100644 --- a/packages/component-manuscript/src/molecules/Expandable.js +++ b/packages/component-manuscript/src/molecules/Expandable.js @@ -1,17 +1,20 @@ import React from 'react' -import styled, { css } from 'styled-components' +import styled from 'styled-components' import { th, Icon } from '@pubsweet/ui' import { compose, withState, withHandlers } from 'recompose' -const Expandable = ({ expanded, label, children, toggle }) => ( +const Expandable = ({ expanded, label, children, toggle, rightHTML }) => ( <Root expanded={expanded}> <Header expanded={expanded} onClick={toggle}> - <Chevron expanded={expanded}> - <Icon primary size={3}> - chevron_up - </Icon> - </Chevron> - <SectionLabel>{label}</SectionLabel> + <LeftDetails> + <Chevron expanded={expanded}> + <Icon primary size={3}> + chevron_up + </Icon> + </Chevron> + <SectionLabel>{label}</SectionLabel> + </LeftDetails> + {rightHTML && <RightDetails>{rightHTML}</RightDetails>} </Header> {expanded && <ChildrenContainer>{children}</ChildrenContainer>} </Root> @@ -51,18 +54,13 @@ const ChildrenContainer = styled.div` const Header = styled.div` align-items: center; + border-width: 0 0 ${th('borderWidth')} 0; + border-style: ${th('borderStyle')}; + border-color: ${th('colorBorder')}; cursor: pointer; display: flex; justify-content: flex-start; - border-width: 0 0 ${th('borderWidth')} 0; - border-style: ${th('borderStyle')}; - border-color: transparent; - ${({ expanded }) => - expanded && - css` - border-color: ${th('colorBorder')}; - margin-bottom: calc(${th('subGridUnit')} * 3); - `}; + margin-bottom: calc(${th('subGridUnit')} * 3); ` const Root = styled.div` @@ -71,4 +69,20 @@ const Root = styled.div` flex-direction: column; transition: all 0.3s; ` + +const LeftDetails = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + flex: ${({ flex }) => flex || 1}; +` + +const RightDetails = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + flex: ${({ flex }) => flex || 1}; +` // #endregion diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index eb0acef37e9e4ef63e517686c9f64606381939b8..843736d4cec859d5ca50449aac9bb0f6bb106ad0 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -49,14 +49,14 @@ const DashboardCard = ({ <Card id={customId}> <ListView> <Top> - <LeftDetails flex="5"> + <LeftDetails flex={6}> <ManuscriptId>{`ID ${customId}`}</ManuscriptId> <Title title={title} dangerouslySetInnerHTML={{ __html: title }} // eslint-disable-line /> </LeftDetails> - <RightDetails flex="2"> + <RightDetails flex={2}> <ZipFiles archiveName={`ID-${project.customId}`} disabled={!hasFiles} @@ -81,11 +81,11 @@ const DashboardCard = ({ </RightDetails> </Top> <Bottom> - <LeftDetails flex="3"> + <LeftDetails flex={3}> <Status>{mapStatusToLabel(project)}</Status> <DateField>{submitted || ''}</DateField> </LeftDetails> - <RightDetails flex="4"> + <RightDetails flex={4}> <ManuscriptType title={manuscriptMeta}> {manuscriptMeta} </ManuscriptType> @@ -257,6 +257,7 @@ const ManuscriptId = styled.div` text-align: left; text-transform: uppercase; white-space: nowrap; + flex: 1; ` const Details = styled.div` @@ -331,7 +332,7 @@ const Title = styled.div` text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - flex: 1; + flex: 10; ` const Status = styled.div` diff --git a/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js b/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js index 6858e83d774b36b4b419de5bb1031b88cea06b2a..1bac07026c7619099c8e8ca46b936174e6fda08b 100644 --- a/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js +++ b/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js @@ -147,11 +147,12 @@ const Root = styled.div` margin-left: ${th('gridUnit')}; ` -const HEName = styled.div`` +const HEName = styled.div` + text-decoration: underline; +` const HEActions = styled.div` ${defaultText}; - text-transform: uppercase; display: flex; align-items: center; cursor: pointer; @@ -173,5 +174,6 @@ const AssignButton = styled(Button)` background-color: ${th('colorPrimary')}; height: calc(${th('subGridUnit')}*5); text-align: center; + text-transform: uppercase; ` // #endregion diff --git a/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js b/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js index 7aa400252509bd60d01abca4765389e05cd34785..aebc1000d766066fc1648a6a1f7df2e75c1387cb 100644 --- a/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js +++ b/packages/components-faraday/src/components/Invitations/ReviewerBreakdown.js @@ -24,7 +24,10 @@ const reviewerReduce = (acc, r) => ({ }) const invitationReduce = (acc, i) => { - const key = i.isAccepted ? 'accepted' : 'declined' + let key = 'pending' + if (i.hasAnswer) { + key = i.isAccepted ? 'accepted' : 'declined' + } return { ...acc, [key]: acc[key] + 1, diff --git a/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js b/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js new file mode 100644 index 0000000000000000000000000000000000000000..1e6e44fcdbaaa46724c3c5c8ad2880e89324a870 --- /dev/null +++ b/packages/components-faraday/src/components/Reviewers/ReviewersDetailsList.js @@ -0,0 +1,211 @@ +import React from 'react' +import moment from 'moment' +import { pick } from 'lodash' +import { connect } from 'react-redux' +import { th, Icon } from '@pubsweet/ui' +import styled, { withTheme } from 'styled-components' +import { compose, withHandlers, withProps } from 'recompose' + +import { revokeReviewer, inviteReviewer } from '../../redux/reviewers' + +const ResendRevoke = withTheme( + ({ theme, showConfirmResend, showConfirmRevoke, status }) => ( + <ActionButtons> + <div onClick={showConfirmResend}> + <Icon color={theme.colorPrimary}>refresh-cw</Icon> + </div> + {status === 'pending' && ( + <div onClick={showConfirmRevoke}> + <Icon color={theme.colorPrimary}>x-circle</Icon> + </div> + )} + </ActionButtons> + ), +) + +const ReviewersList = ({ + renderAcceptedLabel, + reviewers, + showConfirmResend, + showConfirmRevoke, + renderTimestamp, +}) => + reviewers.length > 0 && ( + <Root> + <ScrollContainer> + {reviewers.map((r, index) => ( + <ReviewerItem key={r.invitationId}> + <Column flex={3}> + <div> + <ReviewerName>{r.name}</ReviewerName> + {r.status === 'accepted' && ( + <AcceptedReviewer> + {renderAcceptedLabel(index)} + </AcceptedReviewer> + )} + </div> + <ReviewerEmail>{r.email}</ReviewerEmail> + </Column> + <Column> + <StatusText>{r.status}</StatusText> + <DateText>{renderTimestamp(r.timestamp)}</DateText> + </Column> + {r.status === 'pending' ? ( + <ResendRevoke + showConfirmResend={showConfirmResend(r)} + showConfirmRevoke={showConfirmRevoke(r.invitationId)} + status={r.status} + /> + ) : ( + <Column /> + )} + </ReviewerItem> + ))} + </ScrollContainer> + </Root> + ) + +export default compose( + connect(null, { inviteReviewer, revokeReviewer }), + withProps(({ reviewers = [] }) => ({ + firstAccepted: reviewers.findIndex(r => r.status === 'accepted'), + })), + withHandlers({ + renderTimestamp: () => timestamp => { + const today = moment() + const stamp = moment(timestamp) + const duration = moment.duration(today.diff(stamp)) + + if (duration.asDays() < 1) { + return `${duration.humanize()} ago` + } + return stamp.format('DD.MM.YYYY') + }, + goBackToReviewers: ({ showModal, hideModal, collectionId }) => () => { + showModal({ + collectionId, + type: 'invite-reviewers', + onConfirm: () => { + hideModal() + }, + }) + }, + renderAcceptedLabel: ({ firstAccepted }) => index => + `Reviewer ${index - firstAccepted + 1}`, + }), + withHandlers({ + showConfirmResend: ({ + showModal, + goBackToReviewers, + inviteReviewer, + collectionId, + }) => reviewer => () => { + showModal({ + title: 'Resend reviewer invite', + confirmText: 'Resend', + onConfirm: () => { + inviteReviewer( + pick(reviewer, ['email', 'firstName', 'lastName', 'affiliation']), + collectionId, + ).then(goBackToReviewers, goBackToReviewers) + }, + onCancel: goBackToReviewers, + }) + }, + showConfirmRevoke: ({ + showModal, + hideModal, + goBackToReviewers, + revokeReviewer, + collectionId, + }) => invitationId => () => { + showModal({ + title: 'Unassign Reviewer', + confirmText: 'Unassign', + onConfirm: () => { + revokeReviewer(invitationId, collectionId).then( + goBackToReviewers, + goBackToReviewers, + ) + }, + onCancel: goBackToReviewers, + }) + }, + }), +)(ReviewersList) + +// #region styled-components +const ReviewerEmail = styled.span` + font-size: ${th('fontSizeBaseSmall')}; + color: ${th('colorPrimary')}; + font-family: ${th('fontReading')}; +` + +const ReviewerName = ReviewerEmail.extend` + font-size: ${th('fontSizeBase')}; + text-decoration: underline; +` + +const AcceptedReviewer = ReviewerEmail.extend` + font-weight: bold; + margin-left: ${th('subGridUnit')}; +` + +const StatusText = ReviewerEmail.extend` + text-transform: uppercase; +` + +const DateText = ReviewerEmail.extend`` + +const Column = styled.div` + align-items: flex-start; + display: flex; + flex-direction: column; + justify-content: center; + flex: ${({ flex }) => flex || 1}; +` + +const ReviewerItem = styled.div` + align-items: center; + border-bottom: ${th('borderDefault')}; + display: flex; + justify-content: space-between; + padding: ${th('subGridUnit')} calc(${th('subGridUnit')} * 2); + + &:hover { + background-color: ${th('colorSecondary')}; + } +` + +const ActionButtons = styled.div` + align-items: center; + display: flex; + flex: 1; + justify-content: space-evenly; + opacity: 0; + + div { + cursor: pointer; + } + + ${ReviewerItem}:hover & { + opacity: 1; + } +` + +const ScrollContainer = styled.div` + align-self: stretch; + flex: 1; + overflow: auto; +` +const Root = styled.div` + align-items: stretch; + align-self: stretch; + background-color: ${th('backgroundColorReverse')}; + border: ${th('borderDefault')}; + display: flex; + flex-direction: column; + justify-content: flex-start; + height: 25vh; +` +// #endregion