diff --git a/packages/component-manuscript/src/atoms/index.js b/packages/component-manuscript/src/atoms/index.js index 69667e73d9e7019faeb0e2117e287be51cfc5b61..af47591164adbddb4a11b8bb511c14a6905e67cd 100644 --- a/packages/component-manuscript/src/atoms/index.js +++ b/packages/component-manuscript/src/atoms/index.js @@ -8,10 +8,10 @@ const defaultText = css` ` const Root = styled.div` display: flex; - flex-direction: column; + flex-direction: row; margin: auto; - max-width: 60em; ` + const Title = styled.div` ${defaultText}; font-size: ${th('fontSizeBase')}; @@ -20,6 +20,17 @@ const Title = styled.div` text-align: left; ` +const Container = styled.div` + flex: ${({ flex }) => flex || 1}; + padding: 0 ${th('subGridUnit')}; +` + +const SideBar = styled.div` + flex: ${({ flex }) => flex || 1}; + background-color: ${th('colorBackground')}; + padding: 0 ${th('subGridUnit')}; +` + const Header = styled.div` align-items: center; display: flex; @@ -57,4 +68,51 @@ const ManuscriptId = styled.div` white-space: nowrap; ` -export { Root, BreadCrumbs, Header, Title, ManuscriptId } +const LeftDetails = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + flex: ${({ flex }) => flex || 1}; + max-width: 75%; +` + +const RightDetails = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + flex: ${({ flex }) => flex || 1}; + max-width: 75%; +` + +const StatusLabel = styled.div` + border: ${th('borderDefault')}; + ${defaultText}; + font-weight: bold; + padding: 0 0.5em; + text-align: left; + text-transform: capitalize; + line-height: 1.5; + color: ${th('colorPrimary')}; +` + +const DateField = styled.span` + ${defaultText}; + margin: 0 ${th('subGridUnit')}; + text-align: left; +` + +export { + Root, + BreadCrumbs, + Header, + Title, + ManuscriptId, + Container, + SideBar, + LeftDetails, + RightDetails, + StatusLabel, + DateField, +} diff --git a/packages/component-manuscript/src/components/ManuscriptDetails.js b/packages/component-manuscript/src/components/ManuscriptDetails.js new file mode 100644 index 0000000000000000000000000000000000000000..fbef181efe918f594fa3bf896e622113241ca8eb --- /dev/null +++ b/packages/component-manuscript/src/components/ManuscriptDetails.js @@ -0,0 +1,15 @@ +import React, { Fragment } from 'react' +// import { AuthorsWithTooltip } from '@pubsweet/ui' + +import { Title } from '../atoms' + +import AuthorsWithTooltip from '../molecules/AuthorsWithTooltip' + +const ManuscriptDetails = ({ version, project }) => ( + <Fragment> + <Title dangerouslySetInnerHTML={{ __html: version.metadata.title }} /> + <AuthorsWithTooltip authors={project.authors} /> + </Fragment> +) + +export default ManuscriptDetails diff --git a/packages/component-manuscript/src/components/ManuscriptLayout.js b/packages/component-manuscript/src/components/ManuscriptLayout.js index 3935b6404e23fe80020d82f550e8106cf7e66973..7c852d0b8b10e13ee83053da03f3897a090347ad 100644 --- a/packages/component-manuscript/src/components/ManuscriptLayout.js +++ b/packages/component-manuscript/src/components/ManuscriptLayout.js @@ -1,6 +1,14 @@ import React from 'react' -import { Root, BreadCrumbs, Title, Header, ManuscriptId } from '../atoms' +import ManuscriptDetails from './ManuscriptDetails' +import { + Root, + BreadCrumbs, + Header, + ManuscriptId, + Container, + SideBar, +} from '../atoms' const ManuscriptLayout = ({ currentUser, @@ -9,14 +17,17 @@ const ManuscriptLayout = ({ version, }) => ( <Root> - <Header> - <BreadCrumbs> - <span>Dashboard</span> - <span>Manuscript Details</span> - </BreadCrumbs> - <ManuscriptId>{`- ID ${project.customId}`}</ManuscriptId> - </Header> - <Title dangerouslySetInnerHTML={{ __html: version.metadata.title }} /> + <Container flex={3}> + <Header> + <BreadCrumbs> + <span>Dashboard</span> + <span>Manuscript Details</span> + </BreadCrumbs> + <ManuscriptId>{`- ID ${project.customId}`}</ManuscriptId> + </Header> + <ManuscriptDetails project={project} version={version} /> + </Container> + <SideBar flex={1}>Sidebar</SideBar> </Root> ) diff --git a/packages/component-manuscript/src/molecules/AuthorsWithTooltip.js b/packages/component-manuscript/src/molecules/AuthorsWithTooltip.js new file mode 100644 index 0000000000000000000000000000000000000000..fb160e4f8bb4c08c0c1e55ccbb779eff3d5c047d --- /dev/null +++ b/packages/component-manuscript/src/molecules/AuthorsWithTooltip.js @@ -0,0 +1,172 @@ +import React from 'react' +import { th } from '@pubsweet/ui' +import 'react-tippy/dist/tippy.css' +import { Tooltip } from 'react-tippy' +import styled, { ThemeProvider, withTheme, css } from 'styled-components' + +const DefaultTooltip = ({ + authorName, + email, + affiliation, + isSubmitting, + isCorresponding, + theme, +}) => ( + <ThemeProvider theme={theme}> + <TooltipRoot> + <TooltipRow> + <Name>{authorName}</Name> + {isSubmitting && <SpecialAuthor>Submitting Author</SpecialAuthor>} + {isCorresponding && + !isSubmitting && <SpecialAuthor>Corresponding Author</SpecialAuthor>} + </TooltipRow> + <TooltipRow> + <AuthorDetails>{email}</AuthorDetails> + </TooltipRow> + <TooltipRow> + <AuthorDetails>{affiliation}</AuthorDetails> + </TooltipRow> + </TooltipRoot> + </ThemeProvider> +) + +const DefaultLabel = ({ + firstName, + lastName, + isSubmitting, + isCorresponding, + arr, + index, +}) => ( + <Author> + <AuthorName>{`${firstName} ${lastName}`}</AuthorName> + {isSubmitting && <AuthorStatus>SA</AuthorStatus>} + {isCorresponding && <AuthorStatus>CA</AuthorStatus>} + {arr.length - 1 === index ? '' : ','} + </Author> +) + +const TooltipComponent = ({ children, component: Component, ...rest }) => ( + <Tooltip arrow html={<Component {...rest} />} position="bottom"> + {children} + </Tooltip> +) + +const AuthorTooltip = withTheme(TooltipComponent) + +const AuthorsWithTooltip = ({ + authors = [], + theme, + tooltipComponent = DefaultTooltip, + labelComponent: DefaultComponent = DefaultLabel, +}) => ( + <AuthorList> + {authors.map( + ( + { + affiliation = '', + firstName = '', + lastName = '', + email = '', + userId, + isSubmitting, + isCorresponding, + ...rest + }, + index, + arr, + ) => ( + <AuthorTooltip + affiliation={affiliation} + authorName={`${firstName} ${lastName}`} + component={tooltipComponent} + email={email} + isCorresponding={isCorresponding} + isSubmitting={isSubmitting} + key={userId} + > + <DefaultComponent + arr={arr} + firstName={firstName} + index={index} + isCorresponding={isCorresponding} + isSubmitting={isSubmitting} + lastName={lastName} + /> + </AuthorTooltip> + ), + )} + </AuthorList> +) + +export default AuthorsWithTooltip + +// #region styled-components +const defaultText = css` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` + +const AuthorList = styled.span` + ${defaultText}; + text-align: left; + display: flex; + flex-direction: row; + justify-content: flex-start; +` + +const AuthorName = styled.span` + text-decoration: underline; + padding-left: 2px; + cursor: default; +` +const Author = styled.div` + padding-right: ${th('subGridUnit')}; +` + +const AuthorStatus = styled.span` + border: ${th('borderDefault')}; + ${defaultText}; + text-align: center; + text-transform: uppercase; + padding: 0 2px; + margin-left: 4px; +` + +const TooltipRoot = styled.div` + align-items: flex-start; + display: flex; + flex-direction: column; + justify-content: center; + padding: calc(${th('subGridUnit')}*2); +` + +const TooltipRow = styled.div` + align-items: center; + display: flex; + justify-content: flex-start; + margin: ${th('subGridUnit')} 0; +` + +const Name = styled.span` + color: ${th('colorSecondary')}; + font-family: ${th('fontHeading')}; + font-size: ${th('fontSizeBase')}; +` + +const AuthorDetails = styled.span` + color: ${th('colorSecondary')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` + +const SpecialAuthor = styled.span` + background-color: ${th('colorBackground')}; + color: ${th('colorTextPlaceholder')}; + font-family: ${th('fontInterface')}; + font-size: ${th('fontSizeBaseSmall')}; + font-weight: bold; + margin-left: ${th('subGridUnit')}; + padding: 0 ${th('subGridUnit')}; +` +// #endregion diff --git a/packages/components-faraday/src/components/Dashboard/AuthorTooltip.js b/packages/components-faraday/src/components/Dashboard/AuthorTooltip.js index f358015aaee5ecee846d6933bb72840e48e82682..7692fb74b0075eb846004eb2b3a84f788da705d8 100644 --- a/packages/components-faraday/src/components/Dashboard/AuthorTooltip.js +++ b/packages/components-faraday/src/components/Dashboard/AuthorTooltip.js @@ -1,9 +1,7 @@ import React from 'react' import { th } from '@pubsweet/ui' import { Tooltip } from 'react-tippy' -import styled, { ThemeProvider } from 'styled-components' - -import theme from '../../../../xpub-faraday/app/theme' +import styled, { ThemeProvider, withTheme } from 'styled-components' const AuthorTooltip = ({ authorName, @@ -11,6 +9,7 @@ const AuthorTooltip = ({ affiliation, isSubmitting, isCorresponding, + theme, }) => ( <ThemeProvider theme={theme}> <TooltipRoot> @@ -36,7 +35,7 @@ const TooltipComponent = ({ children, ...rest }) => ( </Tooltip> ) -export default TooltipComponent +export default withTheme(TooltipComponent) // #region styled-components const TooltipRoot = styled.div` @@ -44,14 +43,14 @@ const TooltipRoot = styled.div` display: flex; flex-direction: column; justify-content: center; - padding: 10px 15px; + padding: calc(${th('subGridUnit')}*2); ` const TooltipRow = styled.div` align-items: center; display: flex; justify-content: flex-start; - margin: 3px 0; + margin: ${th('subGridUnit')} 0; ` const AuthorName = styled.span` @@ -72,8 +71,8 @@ const SpecialAuthor = styled.span` font-family: ${th('fontInterface')}; font-size: ${th('fontSizeBaseSmall')}; font-weight: bold; - margin-left: 5px; - padding: 0 5px; + margin-left: ${th('subGridUnit')}; + padding: 0 ${th('subGridUnit')}; ` // #endregion diff --git a/packages/components-faraday/src/components/Reviewers/InviteReviewers.js b/packages/components-faraday/src/components/Reviewers/InviteReviewers.js index 600d264a07f23873b542267d0abc5ea2cc50949d..557a86d74a3d7611fa11bed856b3c5eefccb0f12 100644 --- a/packages/components-faraday/src/components/Reviewers/InviteReviewers.js +++ b/packages/components-faraday/src/components/Reviewers/InviteReviewers.js @@ -11,10 +11,11 @@ import { import { ReviewerBreakdown } from '../Invitations' import { ReviewerForm, ReviewersList } from './' import { - getCollectionReviewers, selectReviewers, - selectFetchingReviewers, selectFetchingInvite, + selectReviewersError, + selectFetchingReviewers, + getCollectionReviewers, } from '../../redux/reviewers' const InviteReviewers = ({ showInviteModal }) => ( @@ -26,8 +27,9 @@ const InviteReviewersModal = compose( connect( state => ({ reviewers: selectReviewers(state), - fetchingReviewers: selectFetchingReviewers(state), + reviewerError: selectReviewersError(state), fetchingInvite: selectFetchingInvite(state), + fetchingReviewers: selectFetchingReviewers(state), }), { getCollectionReviewers }, ), @@ -56,6 +58,7 @@ const InviteReviewersModal = compose( invitations, collectionId, getReviewers, + reviewerError, fetchingInvite, fetchingReviewers, }) => ( @@ -71,6 +74,7 @@ const InviteReviewersModal = compose( collectionId={collectionId} getReviewers={getReviewers} isFetching={fetchingInvite} + reviewerError={reviewerError} reviewers={reviewers} /> diff --git a/packages/components-faraday/src/components/Reviewers/ReviewerForm.js b/packages/components-faraday/src/components/Reviewers/ReviewerForm.js index 8234bcad106f141eb1a5873889b150822fc742c3..02ec4448e4c4d718aff398db8903efc5d09e59eb 100644 --- a/packages/components-faraday/src/components/Reviewers/ReviewerForm.js +++ b/packages/components-faraday/src/components/Reviewers/ReviewerForm.js @@ -11,12 +11,13 @@ import { inviteReviewer } from '../../redux/reviewers' import { ValidatedTextField } from '../AuthorList/FormItems' const ReviewerForm = ({ - clearForm, - selectReviewer, - handleSubmit, users, + clearForm, isFetching, + handleSubmit, filteredUsers, + reviewerError, + selectReviewer, }) => ( <Root> <Row> @@ -27,6 +28,11 @@ const ReviewerForm = ({ <ValidatedTextField label="First name" name="firstName" /> <ValidatedTextField label="Affiliation" name="affiliation" /> </Row> + {reviewerError && ( + <CenterRow> + <Err>{reviewerError}</Err> + </CenterRow> + )} <ButtonsContainer> <FormButton onClick={clearForm}>Clear</FormButton> {isFetching ? ( @@ -92,6 +98,12 @@ const FormButton = styled(Button)` margin: ${th('subGridUnit')}; ` +const Err = styled.span` + color: ${th('colorError')}; + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` + const ButtonsContainer = styled.div` display: flex; justify-content: flex-end; @@ -111,6 +123,10 @@ const Row = styled.div` flex-direction: row; ` +const CenterRow = Row.extend` + justify-content: center; +` + const Root = styled.div` align-self: stretch; border: ${th('borderDefault')}; diff --git a/packages/components-faraday/src/components/utils.js b/packages/components-faraday/src/components/utils.js index 54cd85c4ada4e7724bf918000267b8019a442a3c..e4734a369ac88f22b61fcc490d87b87e547ddc83 100644 --- a/packages/components-faraday/src/components/utils.js +++ b/packages/components-faraday/src/components/utils.js @@ -76,12 +76,7 @@ const emailRegex = new RegExp( export const emailValidator = value => emailRegex.test(value) ? undefined : 'Invalid email' -const alreadyAnswered = `You have already answered this invitation.` export const redirectToError = redirectFn => err => { const errorText = get(JSON.parse(err.response), 'error') - if (errorText.includes('has already been answered')) { - redirectFn('/error-page', alreadyAnswered) - } else { - redirectFn('/error-page', 'Oops! Something went wrong.') - } + redirectFn('/error-page', errorText || 'Oops! Something went wrong.') } diff --git a/packages/components-faraday/src/redux/reviewers.js b/packages/components-faraday/src/redux/reviewers.js index 51d559637995a1280ca6955a8467058c6b063ed2..7678362184dadde9df289656d5e1a47763da2874 100644 --- a/packages/components-faraday/src/redux/reviewers.js +++ b/packages/components-faraday/src/redux/reviewers.js @@ -68,6 +68,7 @@ const initialState = { // selectors export const selectReviewers = state => get(state, 'reviewers.reviewers') || [] +export const selectReviewersError = state => get(state, 'reviewers.error') export const selectFetchingReviewers = state => get(state, 'reviewers.fetching.reviewers') || false export const selectFetchingInvite = state => @@ -98,7 +99,10 @@ export const inviteReviewer = (reviewerData, collectionId) => dispatch => { return create(`/collections/${collectionId}/invitations`, { ...reviewerData, role: 'reviewer', - }).then(() => dispatch(inviteSuccess()), err => dispatch(inviteError(err))) + }).then( + () => dispatch(inviteSuccess()), + err => dispatch(inviteError(get(JSON.parse(err.response), 'error'))), + ) } export const setReviewerPassword = reviewerBody => dispatch => { @@ -205,6 +209,7 @@ export default (state = initialState, action = {}) => { ...state.fetching, invite: false, }, + error: null, } case INVITE_REVIEWER_ERROR: return { diff --git a/packages/xpub-faraday/app/app.js b/packages/xpub-faraday/app/app.js index d390b0b6b0d3371cc56a8a379d9082bafce8c542..0988df482437853ff3bbe0ee9f20354238071b85 100644 --- a/packages/xpub-faraday/app/app.js +++ b/packages/xpub-faraday/app/app.js @@ -1,6 +1,5 @@ import React from 'react' import ReactDOM from 'react-dom' -import 'react-tippy/dist/tippy.css' import { AppContainer } from 'react-hot-loader' import createHistory from 'history/createBrowserHistory'