Skip to content
Snippets Groups Projects
Commit fdfc69d5 authored by Vlad's avatar Vlad
Browse files

Merge branch 'master' of gitlab.coko.foundation:xpub/xpub-faraday

parents 95125582 b0f7914f
No related branches found
No related tags found
No related merge requests found
Showing
with 434 additions and 84 deletions
......@@ -8,7 +8,7 @@ module.exports = {
'http://localhost:3000/invite',
},
roles: {
global: ['admin', 'editorInChief', 'author'],
global: ['admin', 'editorInChief', 'author', 'handlingEditor'],
collection: ['handlingEditor', 'reviewer'],
inviteRights: {
admin: ['admin', 'editorInChief', 'author'],
......
......@@ -4,6 +4,7 @@ const helpers = require('../helpers/helpers')
const teamHelper = require('../helpers/Team')
const mailService = require('pubsweet-component-mail-service')
const inviteHelper = require('../helpers/Invitation')
const collHelper = require('../helpers/Collection')
const configRoles = config.get('roles')
......@@ -53,8 +54,9 @@ module.exports = async (
}
}
let collection
try {
await models.Collection.find(collectionId)
collection = await models.Collection.find(collectionId)
} catch (e) {
const notFoundError = await helpers.handleNotFoundError(e, 'collection')
return res.status(notFoundError.status).json({
......@@ -65,7 +67,7 @@ module.exports = async (
try {
let user = await models.User.findByEmail(email)
let team = teamHelper.getTeamByGroupAndCollection(
let team = await teamHelper.getTeamByGroupAndCollection(
collectionId,
role,
models.Team,
......@@ -77,11 +79,13 @@ module.exports = async (
collectionId,
role,
)
user = await models.User.findByEmail(email)
} else {
user.teams = user.teams || []
user.teams.push(team.id)
user = await user.save()
}
// getting the updated user from the DB - creating a team also updates the user
user = await models.User.findByEmail(email)
if (user.invitations === undefined) {
user = await inviteHelper.setupInvitation(
user,
......@@ -89,6 +93,7 @@ module.exports = async (
collectionId,
team.id,
)
await collHelper.addAssignedPeople(collection, user, role)
} else {
const matchingInvitation = inviteHelper.getMatchingInvitation(
user.invitations,
......@@ -102,6 +107,7 @@ module.exports = async (
collectionId,
team.id,
)
await collHelper.addAssignedPeople(collection, user, role)
}
}
......
const invitationHelper = require('./Invitation')
module.exports = {
addAssignedPeople: async (collection, user, role) => {
collection.assignedPeople = collection.assignedPeople || []
const matchingInvitation = invitationHelper.getMatchingInvitation(
user.invitations,
collection.id,
role,
)
const assignedPerson = {
id: user.id,
email: user.email,
name: `${user.firstName} ${user.lastName}`,
role,
hasAnswer: matchingInvitation.hasAnswer,
isAccepted: matchingInvitation.isAccepted,
}
collection.assignedPeople.push(assignedPerson)
await collection.save()
},
}
......@@ -74,7 +74,7 @@ const setupEiCTeams = async (models, user) => {
const setupManuscriptTeam = async (models, user, collectionId, role) => {
const teams = await models.Team.all()
user.teams = []
user.teams = user.teams || []
const filteredTeams = teams.filter(
team =>
team.group === role &&
......@@ -89,12 +89,16 @@ const setupManuscriptTeam = async (models, user, collectionId, role) => {
try {
team = await team.updateProperties(team)
team = await team.save()
user.teams.push(team.id)
await user.save()
return team
} catch (e) {
logger.error(e)
}
} else {
const team = await createNewTeam(collectionId, role, user.id, models.Team)
user.teams.push(team.id)
await user.save()
return team
}
}
......
......@@ -84,7 +84,7 @@ const createNewUser = async (
UserModel,
role,
) => {
const username = uuid.v4().slice(0, 8)
const username = email
const password = uuid.v4()
const userBody = {
username,
......
......@@ -179,6 +179,7 @@ describe('Post invite route handler', () => {
const data = JSON.parse(res._getData())
expect(data.email).toEqual(body.email)
expect(data.invitations[0].collectionId).toEqual(req.params.collectionId)
expect(standardCollection.assignedPeople).toHaveLength(1)
})
it('should return success when the handlingEditor invites a reviewer with a collection', async () => {
const body = {
......
......@@ -40,5 +40,5 @@ const ModalRoot = styled.div`
justify-content: center;
background-color: ${({ overlayColor }) =>
overlayColor || 'rgba(0, 0, 0, 0.8)'};
z-index: ${({ theme }) => theme.modalIndex};
/* z-index: ${({ theme }) => theme.modalIndex}; */
`
import React from 'react'
import { omit } from 'lodash'
import { connect } from 'react-redux'
import Modal from './Modal'
import { setModalVisibility } from '../redux/modal'
import { showModal, hideModal } from '../redux/modal'
const mapState = state => ({
modalVisible: state.modal.visible,
modalsVisibility: omit(state.modal, 'props'),
modalProps: state.modal.props,
})
const mapDispatch = dispatch => ({
hideModal: () => dispatch(setModalVisibility(false)),
showModal: (modalProps = {}) =>
dispatch(setModalVisibility(true, modalProps)),
const mapDispatch = modalKey => (dispatch, propss) => ({
hideModal: () => dispatch(hideModal()),
showModal: (modalProps = {}) => dispatch(showModal(modalKey, modalProps)),
})
const withModal = ({
modalKey,
modalComponent: Component,
overlayColor,
}) => WrappedComponent =>
connect(mapState, mapDispatch)(
({ modalVisible, modalProps, hideModal, ...rest }) => (
<div>
{modalVisible && (
connect(mapState, mapDispatch(modalKey))(
({ modalsVisibility, modalProps, hideModal, ...rest }) => (
<React.Fragment>
{modalsVisibility[modalKey] && (
<Modal
{...modalProps}
component={Component}
......@@ -31,7 +32,7 @@ const withModal = ({
/>
)}
<WrappedComponent hideModal={hideModal} {...rest} />
</div>
</React.Fragment>
),
)
......
const initialState = {
visible: false,
props: {},
}
const SET_MODAL_VISIBILTY = 'modal/SET_MODAL_VISIBILTY'
const SHOW_MODAL = 'modal/SHOW_MODAL'
const HIDE_MODAL = 'modal/HIDE_MODAL'
export const setModalVisibility = (visible, props = {}) => ({
type: SET_MODAL_VISIBILTY,
export const showModal = (modalKey, props = {}) => ({
type: SHOW_MODAL,
payload: {
visible,
modalKey,
props,
},
})
export const hideModal = () => ({
type: HIDE_MODAL,
})
export const getModalVisibility = (state, modalKey) => state[modalKey]
export default (state = initialState, action = {}) => {
switch (action.type) {
case SHOW_MODAL:
return {
[action.payload.modalKey]: true,
props: action.payload.props,
}
case HIDE_MODAL:
return initialState
default:
return state
}
......
......@@ -6,7 +6,6 @@
"dependencies": {
"@pubsweet/ui": "^3.1.0",
"moment": "^2.20.1",
"portal-modal": "^1.0.3",
"prop-types": "^15.5.10",
"react": "^16.1.0",
"react-dnd": "^2.5.4",
......
......@@ -13,7 +13,7 @@ const emailValidator = value =>
const AddUserForm = ({ roles, journal, error }) => {
const roleOptions = roles.filter(r =>
['editorInChief', 'author', 'admin'].includes(r.value),
['editorInChief', 'author', 'admin', 'handlingEditor'].includes(r.value),
)
return (
<div>
......
import React from 'react'
import styled, { css } from 'styled-components'
import { Button, th } from '@pubsweet/ui'
import { compose, withHandlers } from 'recompose'
import { withModal } from 'pubsweet-component-modal/src/components'
import HEModal from './AssignHEModal'
const AssignEditor = ({ assign }) => (
<ActionButtons onClick={assign}>ASSIGN</ActionButtons>
)
export default compose(
withModal({
modalKey: 'assignHEmodal',
modalComponent: HEModal,
}),
withHandlers({
assign: ({ showModal, collectionId }) => () => {
showModal({
collectionId,
})
},
}),
)(AssignEditor)
// #region styled-components
const defaultText = css`
color: ${th('colorText')};
font-family: ${th('fontReading')};
font-size: ${th('fontSizeBaseSmall')};
`
const ActionButtons = styled(Button)`
${defaultText};
align-items: center;
background-color: ${th('colorPrimary')};
color: ${th('colorTextReverse')};
text-align: center;
height: calc(${th('subGridUnit')}*5);
`
// #endregion
/* eslint react/prefer-stateless-function: 0 */
import React from 'react'
import { th } from '@pubsweet/ui'
import { compose } from 'recompose'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { handlingEditors, assignHandlingEditor } from '../../redux/editors'
class AssignHEModal extends React.Component {
state = {
searchInput: '',
}
changeInput = e => {
this.setState({ searchInput: e.target.value })
}
filterEditors = editors => {
const { searchInput } = this.state
return editors.filter(({ firstName, lastName, email }) =>
`${firstName} ${lastName} ${email}`
.toLowerCase()
.includes(searchInput.trim()),
)
}
assignEditor = email => () => {
const { assignHandlingEditor, collectionId, hideModal } = this.props
assignHandlingEditor(email, collectionId).then(hideModal)
}
render() {
const { searchInput } = this.state
const { editors } = this.props
const filteredEditors = this.filterEditors(editors)
return (
<RootModal>
<button onClick={this.props.hideModal}>CLOSE</button>
<ModalTitle>Assign Handling Editor</ModalTitle>
<ModalHeader>
<span>HANDLING EDITORS</span>
<SearchInput
onChange={this.changeInput}
type="text"
value={searchInput}
/>
</ModalHeader>
<ScrollContainer>
<ModalContent>
{filteredEditors.map(({ firstName, lastName, email }, index) => (
<SuggestedEditor
isLast={index === filteredEditors.length - 1}
key={email}
>
<EditorDetails>
<span>{`${firstName} ${lastName}`}</span>
<span>{email}</span>
</EditorDetails>
<AssignButton onClick={this.assignEditor(email)}>
ASSIGN
</AssignButton>
</SuggestedEditor>
))}
</ModalContent>
</ScrollContainer>
</RootModal>
)
}
}
export default compose(
connect(
state => ({
editors: handlingEditors(state),
}),
{ assignHandlingEditor },
),
)(AssignHEModal)
// #region styled-components
const EditorDetails = styled.div`
display: flex;
flex-direction: column;
`
const SuggestedEditor = styled.div`
align-items: center;
border: ${th('borderDefault')};
border-bottom: ${({ isLast }) => (isLast ? th('borderDefault') : 'none')};
display: flex;
justify-content: space-between;
padding: ${th('subGridUnit')};
height: calc(${th('gridUnit')} * 2);
&:hover {
background-color: ${th('colorSecondary')};
}
`
const AssignButton = styled.button`
align-items: center;
color: ${th('colorTextReverse')};
background-color: ${th('colorPrimary')};
display: flex;
justify-content: center;
height: ${th('gridUnit')};
width: calc(${th('gridUnit')} * 4);
opacity: 0;
${SuggestedEditor}:hover & {
opacity: 1;
}
`
const RootModal = styled.div`
align-items: center;
background-color: ${th('colorBackgroundHue')};
display: flex;
flex-direction: column;
justify-content: flex-start;
height: calc(${th('gridUnit')} * 18);
padding: calc(${th('subGridUnit')} * 8) calc(${th('subGridUnit')} * 6);
width: calc(${th('gridUnit')} * 24);
`
const ModalTitle = styled.span`
color: ${th('colorPrimary')};
font-size: ${th('fontSizeHeading4')};
font-family: ${th('fontHeading')};
margin-bottom: calc(${th('subGridUnit')} * 5);
`
const ModalHeader = styled.div`
align-self: stretch;
display: flex;
flex-direction: column;
& span {
color: ${th('colorPrimary')};
font-size: ${th('fontSizeBase')};
margin-bottom: ${th('subGridUnit')};
}
`
const SearchInput = styled.input`
border: 4px solid gray;
height: calc(${th('subGridUnit')} * 5);
padding: ${th('subGridUnit')};
`
const ScrollContainer = styled.div`
align-self: stretch;
flex: 1;
overflow: auto;
border: ${th('borderDefault')};
`
const ModalContent = styled.div`
display: flex;
flex-direction: column;
overflow: auto;
padding: calc(${th('subGridUnit')} * 2) calc(${th('subGridUnit')} * 3);
`
// #endregion
import React from 'react'
import { get } from 'lodash'
import { Button } from '@pubsweet/ui'
import styled from 'styled-components'
import { compose, withHandlers } from 'recompose'
import { withModal } from 'pubsweet-component-modal/src/components'
import AbstractModal from './AbstractModal'
import DashboardItems from './DashboardItems'
import DashboardFilters from './DashboardFilters'
......@@ -49,9 +46,6 @@ const Dashboard = ({
)
export default compose(
withModal({
modalComponent: AbstractModal,
}),
withHandlers({
showAbstractModal: ({ showModal }) => metadata => () => {
showModal({
......@@ -65,16 +59,11 @@ export default compose(
currentUser,
dashboard,
filterItems,
}) => () => {
const userItems = get(currentUser, 'admin')
? dashboard.all
: dashboard.owner
return filterItems(userItems).sort((a, b) => {
}) => () =>
filterItems(dashboard.all).sort((a, b) => {
if (sortOrder === 'newest') return a.created - b.created < 0
return a.created - b.created > 0
})
},
}),
}),
)(Dashboard)
......
import React from 'react'
import { get } from 'lodash'
import PropTypes from 'prop-types'
import { compose, getContext } from 'recompose'
import { Button, Icon, th } from '@pubsweet/ui'
import styled, { css } from 'styled-components'
import { compose, getContext, withHandlers } from 'recompose'
import {
withModal,
ConfirmationModal,
} from 'pubsweet-component-modal/src/components'
import { parseVersion, parseJournalIssue } from './utils'
import styled, { css, withTheme } from 'styled-components'
import ZipFiles from './ZipFiles'
import { parseVersion, parseJournalIssue } from './utils'
import HandlingEditorActions from './HandlingEditorActions'
const DashboardCard = ({
deleteProject,
......@@ -21,6 +17,7 @@ const DashboardCard = ({
showAbstractModal,
journal,
cancelSubmission,
theme,
...rest
}) => {
const { submitted, title, type } = parseVersion(version)
......@@ -87,7 +84,7 @@ const DashboardCard = ({
}
>
Details
<Icon color="#667080">chevron-right</Icon>
<Icon color={theme.colorPrimary}>chevron-right</Icon>
</Details>
) : (
<Details
......@@ -118,49 +115,33 @@ const DashboardCard = ({
arr,
) => (
<Author key={email}>
{isSubmitting && <AuthorStatus>SA</AuthorStatus>}
{isCorresponding &&
!isSubmitting && <AuthorStatus>CA</AuthorStatus>}
<AuthorName>
{firstName} {middleName} {lastName}
</AuthorName>
{isSubmitting && <AuthorStatus>SA</AuthorStatus>}
{isCorresponding &&
!isSubmitting && <AuthorStatus>CA</AuthorStatus>}
{arr.length - 1 === index ? '' : ','}
</Author>
),
)}
</AuthorList>
</Top>
<Bottom>
<LeftDetails flex="5">
<HEText>Handling Editor</HEText>
<HandlingEditorActions project={project} />
</LeftDetails>
</Bottom>
</DetailsView>
)}
</Card>
) : null
}
export default compose(
getContext({ journal: PropTypes.object }),
withModal({
modalComponent: ConfirmationModal,
}),
withHandlers({
cancelSubmission: ({
showModal,
deleteProject,
project,
hideModal,
}) => () => {
const modalConfig = {
onConfirm: () => {
deleteProject(project)
hideModal()
},
dismissable: false,
title: 'Are you sure you want to delete the manuscript?',
subtitle: 'This operation cannot be undone!',
}
showModal(modalConfig)
},
}),
)(DashboardCard)
export default compose(getContext({ journal: PropTypes.object }), withTheme)(
DashboardCard,
)
// #region styled-components
const defaultText = css`
......@@ -191,6 +172,7 @@ const AuthorStatus = styled.span`
text-align: center;
text-transform: uppercase;
padding: 0 2px;
margin-left: 4px;
`
const ActionButtons = styled(Button)`
......@@ -226,7 +208,7 @@ const DetailsView = styled.div`
align-items: center;
border-top: ${th('borderDefault')};
display: flex;
flex-direction: row;
flex-direction: column;
justify-content: space-between;
width: 100%;
`
......@@ -322,9 +304,11 @@ const Status = styled.div`
border: ${th('borderDefault')};
${defaultText};
font-weight: bold;
padding: 0.2em 0.5em;
padding: 0 0.5em;
text-align: left;
text-transform: uppercase;
text-transform: capitalize;
line-height: 1.5;
color: ${th('colorPrimary')};
`
const DateField = styled.span`
......@@ -332,4 +316,10 @@ const DateField = styled.span`
margin: 0 ${th('subGridUnit')};
text-align: left;
`
const HEText = styled.div`
${defaultText};
text-transform: uppercase;
`
// #endregion
......@@ -10,7 +10,7 @@ import { newestFirst, selectCurrentUser } from 'xpub-selectors'
import { createDraftSubmission } from 'pubsweet-component-wizard/src/redux/conversion'
import Dashboard from './Dashboard'
import { getHandlingEditors } from '../../redux/editors'
import withFilters from './withFilters'
export default compose(
......@@ -18,6 +18,7 @@ export default compose(
actions.getCollections(),
actions.getTeams(),
actions.getUsers(),
getHandlingEditors(),
]),
connect(
state => {
......
import React from 'react'
import { get, head } from 'lodash'
import { Icon, th } from '@pubsweet/ui'
import styled, { css, withTheme } from 'styled-components'
import { compose, withHandlers } from 'recompose'
import AssignEditor from './AssignEditor'
const HandlingEditorActions = ({ project, theme, getHandlingEditor }) => {
const handlingEditor = getHandlingEditor()
return (
<Root>
<HEActions>
{handlingEditor ? (
<HEActions>
<HEName>{get(handlingEditor, 'name')}</HEName>
{!handlingEditor.hasAnswer && (
<HEActions>
<Icon color={theme.colorPrimary}>refresh-cw</Icon>
<Icon color={theme.colorPrimary}>x-circle</Icon>
</HEActions>
)}
</HEActions>
) : (
<AssignEditor collectionId={project.id} />
)}
</HEActions>
</Root>
)
}
export default compose(
withTheme,
withHandlers({
getHandlingEditor: ({ project }) => () => {
const assignedEditors = get(project, 'assignedPeople')
if (assignedEditors && assignedEditors.length) {
return head(
assignedEditors.filter(
editor =>
!editor.hasAnswer || (editor.hasAnswer && editor.isAccepted),
),
)
}
return null
},
}),
)(HandlingEditorActions)
// #region styled-components
const defaultText = css`
color: ${th('colorText')};
font-family: ${th('fontReading')};
font-size: ${th('fontSizeBaseSmall')};
`
const Root = styled.div`
margin-left: ${th('gridUnit')};
`
const HEName = styled.div``
const HEActions = styled.div`
${defaultText};
text-transform: uppercase;
display: flex;
align-items: center;
cursor: pointer;
margin-left: ${th('subGridUnit')};
span {
margin-left: ${th('subGridUnit')};
&:hover {
svg {
opacity: 0.8;
}
}
}
`
// #endregion
......@@ -4,7 +4,7 @@ module.exports = {
reducers: {
authors: () => require('./redux/authors').default,
files: () => require('./redux/files').default,
modal: () => require('./redux/modal').default,
editors: () => require('./redux/editors').default,
},
},
}
import { get, create, remove } from 'pubsweet-client/src/helpers/api'
const SET_HANDLING_EDITORS = 'SET_HANDLING_EDITORS'
const setHandlingEditors = editors => ({
type: SET_HANDLING_EDITORS,
editors,
})
export const handlingEditors = state => state.editors
export const getHandlingEditors = () => dispatch =>
get(`/users?handlingEditor=true`).then(res => {
dispatch(setHandlingEditors(res.users))
})
export const assignHandlingEditor = (
email,
collectionId,
resend = false,
) => dispatch =>
create(`/users/invite/${collectionId}`, {
email,
role: 'handlingEditor',
resend,
})
export const revokeHandlingEditor = (collectionId, userId) => dispatch =>
remove(`/collections/${collectionId}/users/${userId}?role=handlingEditor`)
const initialState = []
export default (state = initialState, action = {}) => {
switch (action.type) {
case SET_HANDLING_EDITORS:
return action.editors
default:
return state
}
}
export { default as modal } from './modal'
export { default as authors } from './authors'
export { default as editors } from './editors'
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment