...
 
Commits (43)
import React, { Fragment } from 'react'
import styled from 'styled-components'
import { th, override, validationColor } from '@pubsweet/ui-toolkit'
import { startsWith, toLower, get, head } from 'lodash'
import { startsWith, toLower, get } from 'lodash'
import { withCountries } from 'pubsweet-component-faraday-ui'
import { compose, withState, withHandlers, withProps } from 'recompose'
const filteredCountries = ({ countries, userInput }) =>
countries.filter(o => startsWith(toLower(o.label), toLower(userInput)))
const firstFilteredCountry = ({ countries, userInput }) =>
head(filteredCountries({ countries, userInput }))
const Menu = ({
open,
options,
......@@ -21,6 +18,8 @@ const Menu = ({
handleSelect,
onTextChange,
validationStatus,
handleKeyDown,
cursor,
}) => (
<Fragment>
{open && <CloseOverlay onClick={toggleMenu} />}
......@@ -28,6 +27,7 @@ const Menu = ({
<Input
onChange={onTextChange}
onClick={toggleMenu}
onKeyDown={handleKeyDown}
onKeyUp={onEnter}
placeholder={placeholder}
validationStatus={validationStatus}
......@@ -35,8 +35,12 @@ const Menu = ({
/>
{open && (
<Options>
{options.map(option => (
<Option key={option.value} onClick={handleSelect(option.value)}>
{options.map((option, index) => (
<Option
active={cursor === index}
key={option.value}
onClick={handleSelect(option.value)}
>
{option.label}
</Option>
))}
......@@ -53,7 +57,11 @@ export default compose(
'updateUserInput',
({ value, countryLabel }) => (value ? countryLabel(value) : ''),
),
withState('cursor', 'setCursor', 0),
withState('open', 'updateOptionsVisibility', false),
withProps(({ countries, userInput }) => ({
options: filteredCountries({ countries, userInput }),
})),
withHandlers({
handleSelect: ({
onChange,
......@@ -62,6 +70,7 @@ export default compose(
updateOptionsVisibility,
}) => value => () => {
const country = countryLabel(value)
if (country) {
onChange(value)
updateUserInput(country)
......@@ -73,26 +82,37 @@ export default compose(
toggleMenu: ({ updateOptionsVisibility, open }) => () => {
updateOptionsVisibility(!open)
},
onTextChange: ({ updateUserInput, countryLabel, onChange }) => event => {
onTextChange: ({
updateUserInput,
countryLabel,
onChange,
setCursor,
}) => event => {
const inputValue = get(event, 'target.value', '')
const country = countryLabel(inputValue)
setCursor(0)
updateUserInput(inputValue)
if (!country) {
onChange('')
}
},
onEnter: ({ handleSelect, userInput, countries }) => event => {
onEnter: ({ handleSelect, options, cursor }) => event => {
if (event.which === 13) {
handleSelect(
get(firstFilteredCountry({ countries, userInput }), 'value'),
)()
handleSelect(get(options[cursor], 'value'))()
}
},
handleKeyDown: ({ setCursor, options }) => event => {
// arrow up
if (event.which === 38) {
setCursor(c => Math.max(0, c - 1))
}
// arrow down
if (event.which === 40) {
setCursor(c => Math.min(c + 1, options.length - 1))
}
},
}),
withProps(({ countries, userInput }) => ({
options: filteredCountries({ countries, userInput }),
})),
)(Menu)
// #region styles
......
......@@ -14,6 +14,7 @@ const PersonInvitation = ({
revokeInvitation,
resendInvitation,
person: { name, email },
role,
...rest
}) => (
<Root {...rest}>
......@@ -21,24 +22,26 @@ const PersonInvitation = ({
{!hasAnswer &&
name !== 'Unassigned' && (
<Fragment>
<OpenModal
confirmText="Resend"
isFetching={isFetching}
modalKey={`resend-${id}`}
onConfirm={resendInvitation}
subtitle={email}
title="Resend the invitation?"
>
{showModal => (
<IconButton
fontIcon="resendIcon"
mb={1}
ml={2}
onClick={showModal}
secondary
/>
)}
</OpenModal>
{role !== 'reviewer' && (
<OpenModal
confirmText="Resend"
isFetching={isFetching}
modalKey={`resend-${id}`}
onConfirm={resendInvitation}
subtitle={email}
title="Resend the invitation?"
>
{showModal => (
<IconButton
fontIcon="resendIcon"
mb={1}
ml={2}
onClick={showModal}
secondary
/>
)}
</OpenModal>
)}
<OpenModal
confirmText="Revoke"
isFetching={isFetching}
......
......@@ -78,6 +78,7 @@ const ReviewersTable = ({
{...invitation}
onResend={onResendReviewerInvite}
onRevoke={onRevokeReviewerInvite}
role={invitation.role}
/>
)}
</HiddenCell>
......
import React, { Fragment } from 'react'
import { get } from 'lodash'
import { Field } from 'formik'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { th } from '@pubsweet/ui-toolkit'
import {
compose,
withProps,
withHandlers,
shouldUpdate,
setDisplayName,
} from 'recompose'
const WrappedComponent = compose(
withHandlers({
onChange: ({ field: { onChange, name }, form: { setFieldValue } }) => v => {
if (typeof v === 'object') {
onChange(v)
} else {
setFieldValue(name, v)
}
},
}),
withProps(({ form: { errors, touched, submitCount }, name }) => ({
hasError: (get(touched, name) || submitCount > 0) && get(errors, name),
})),
withProps(({ name, hasError, form: { errors } }) => ({
error: hasError && get(errors, name),
validationStatus: hasError ? 'error' : 'default',
})),
shouldUpdate(
(prev, next) =>
get(prev, 'field.value') !== get(next, 'field.value') ||
get(prev, 'error') !== get(next, 'error'),
),
)(
({
error,
onChange,
validationStatus,
component: Component,
field: { name, value, onBlur },
form: { errors, setFieldValue, touched, values, submitCount },
...props
}) => (
<Fragment>
<Component
name={name}
onBlur={onBlur}
onChange={onChange}
value={value}
{...props}
validationStatus={validationStatus}
/>
<MessageWrapper role="alert">
{error && <ErrorMessage>{error}</ErrorMessage>}
</MessageWrapper>
</Fragment>
),
)
const ValidatedFormField = ({ name, component, validateFn, ...props }) => (
<Field name={name} validate={validateFn}>
{fieldProps => (
<WrappedComponent
component={component}
name={name}
{...props}
{...fieldProps}
/>
)}
</Field>
)
ValidatedFormField.propTypes = {
component: PropTypes.oneOfType([
PropTypes.node,
PropTypes.func,
PropTypes.string,
]).isRequired,
}
export default compose(
setDisplayName('ValidatedFormikField'),
withProps(({ validate = [] }) => ({
validateFn: (value = '') =>
validate.reduce((acc, fn) => acc || fn(value), ''),
})),
shouldUpdate(() => false),
)(ValidatedFormField)
// #region styles
const MessageWrapper = styled.div`
font-family: ${th('fontInterface')};
display: flex;
`
const Message = styled.div`
&:not(:last-child) {
margin-bottom: ${th('gridUnit')};
}
font-size: ${th('fontSizeBaseSmall')};
line-height: ${th('lineHeightBaseSmall')};
`
const ErrorMessage = styled(Message)`
color: ${th('colorError')};
`
// #endregion
import React from 'react'
import { Field } from 'formik'
import { Menu } from '@pubsweet/ui'
const ValidatedMenuField = ({ options, name }) => (
<Field name={name}>
{({ field, form }) => (
<Menu
onChange={v => {
// field.onChange(v)
form.setFieldValue(field.name, v)
}}
options={options}
value={field.value}
/>
)}
</Field>
)
export default ValidatedMenuField
......@@ -51,7 +51,7 @@ export { default as EditorialReportCard } from './EditorialReportCard'
export { default as ReviewerReportAuthor } from './ReviewerReportAuthor'
export { default as PasswordValidation } from './PasswordValidation'
export { default as MenuCountry } from './MenuCountry'
export { default as ValidatedMenuField } from './ValidatedMenuField'
export { default as ValidatedFormField } from './ValidatedFormField'
export { SubmitRevision } from './submissionRevision'
......
import PropTypes from 'prop-types'
import React, { Fragment } from 'react'
import { compose } from 'recompose'
import { ManuscriptFileSection } from 'pubsweet-component-faraday-ui'
import { withFilePreview, withFileDownload } from '../helpers'
import { withFilePreview, withFileDownload } from '../'
const ManuscriptFileList = ({
files: { manuscripts = [], coverLetter = [], supplementary = [] },
......@@ -43,18 +44,15 @@ ManuscriptFileList.propTypes = {
supplementary: PropTypes.arrayOf(PropTypes.object),
}),
/** Callback function fired when delete icon is pressed. */
onDelete: PropTypes.func,
onDelete: PropTypes.func, // eslint-disable-line
/** Callback function fired when download icon is pressed. */
onDownload: PropTypes.func,
onDownload: PropTypes.func, // eslint-disable-line
/** Callback function fired when preview icon is pressed. */
onPreview: PropTypes.func,
onPreview: PropTypes.func, // eslint-disable-line
}
ManuscriptFileList.defaultProps = {
files: {},
onDelete: () => {},
onDownload: () => {},
onPreview: () => {},
}
export default withFilePreview(withFileDownload(ManuscriptFileList))
export default compose(withFilePreview, withFileDownload)(ManuscriptFileList)
......@@ -35,16 +35,14 @@ ManuscriptFileSection.propTypes = {
/** Category name of uploaded files. */
label: PropTypes.string,
/** Callback function fired when download icon is pressed. */
onDownload: PropTypes.func,
onDownload: PropTypes.func, // eslint-disable-line
/** Callback function fired when preview icon is pressed. */
onPreview: PropTypes.func,
onPreview: PropTypes.func, // eslint-disable-line
}
ManuscriptFileSection.defaultProps = {
list: [],
label: '',
onDownload: () => {},
onPreview: () => {},
}
export default ManuscriptFileSection
......@@ -20,12 +20,14 @@
},
"dependencies": {
"body-parser": "^1.17.2",
"chance": "^1.0.13"
"chance": "^1.0.13",
"moment": "^2.23.0"
},
"peerDependencies": {
"@pubsweet/component-send-email": "0.2.4",
"@pubsweet/logger": "^0.0.1",
"pubsweet-server": "^10.0.0",
"@pubsweet/component-send-email": "0.2.4"
"pubsweet-component-jobs": "0.0.1"
},
"devDependencies": {
"apidoc": "^0.17.6",
......
......@@ -10,6 +10,8 @@ const {
deleteFilesS3,
} = require('pubsweet-component-mts-package/src/PackageManager')
const Job = require('pubsweet-component-jobs')
const { last, get, chain, difference } = require('lodash')
const s3Config = get(config, 'pubsweet-component-aws-s3', {})
......@@ -132,6 +134,11 @@ module.exports = models => async (req, res) => {
})),
]
fragment.invitations.forEach(inv => {
Job.cancelQueue(`removal-${inv.userId}-${inv.id}`)
Job.cancelQueue(`reminders-${inv.userId}-${inv.id}`)
})
fragment.invitations = []
fragment.recommendations = []
fragment.revision && delete fragment.revision
......
......@@ -6,6 +6,7 @@ const {
} = require('pubsweet-component-helper-service')
const notifications = require('./emails/notifications')
const Job = require('pubsweet-component-jobs')
module.exports = models => async (req, res) => {
const { collectionId, invitationId, fragmentId } = req.params
......@@ -19,7 +20,8 @@ module.exports = models => async (req, res) => {
const users = await UserModel.all()
const user = users.find(
user => user.accessTokens.invitation === invitationToken,
user =>
user.accessTokens && user.accessTokens.invitation === invitationToken,
)
if (!user) {
return res.status(404).json({
......@@ -67,6 +69,9 @@ module.exports = models => async (req, res) => {
error: 'Unauthorized.',
})
await Job.cancelQueue(`reminders-${user.id}-${invitation.id}`)
await Job.cancelQueue(`removal-${user.id}-${invitation.id}`)
invitation.respondedOn = Date.now()
invitation.hasAnswer = true
invitation.isAccepted = false
......
......@@ -6,6 +6,7 @@ const {
} = require('pubsweet-component-helper-service')
const notifications = require('./emails/notifications')
const Job = require('pubsweet-component-jobs')
module.exports = models => async (req, res) => {
const { collectionId, invitationId, fragmentId } = req.params
......@@ -79,6 +80,9 @@ module.exports = models => async (req, res) => {
UserModel: models.User,
})
await Job.cancelQueue(`reminders-${user.id}-${invitation.id}`)
await Job.cancelQueue(`removal-${user.id}-${invitation.id}`)
return res.status(200).json({ fragment })
} catch (e) {
const notFoundError = await services.handleNotFoundError(e, 'collection')
......
......@@ -33,6 +33,20 @@ const getEmailCopy = ({
lowerContent = `Thank you in advance for taking the time to consider this invitation, as it would not be possible for us to run the journal without the help of our reviewers.<br/><br/>
I am looking forward to hearing from you.`
break
case 'reviewer-resend-invitation-first-reminder':
resend = true
upperContent = `On ${expectedDate} I invited you to review ${titleText}, submitted for possible publication in ${journalName}.<br/><br/>
We'd be grateful if you could submit a decision on whether or not you will be able to review this manuscript using the link below.`
lowerContent = `Thank you in advance for your help with the evaluation of this manuscript.<br/><br/>
We look forward to hearing from you.`
break
case 'reviewer-resend-invitation-second-reminder':
case 'reviewer-resend-invitation-third-reminder':
resend = true
upperContent = `We sent you a request to review ${titleText}; however we have not yet received your decision.
We would appreciate it if you could visit the following link to let us know whether or not you will be able to review this manuscript:`
lowerContent = `Please do not hesitate to contact me if you have any problems with the system.`
break
case 'reviewer-accepted':
paragraph = `We are pleased to inform you that Dr. ${targetUserName} has agreed to review ${titleText}.<br/><br/>
You should receive the report before ${expectedDate}.<br/><br/>
......
const config = require('config')
const { get } = require('lodash')
const { get, forOwn } = require('lodash')
const Email = require('@pubsweet/component-email-templating')
const unsubscribeSlug = config.get('unsubscribe.url')
......@@ -9,14 +10,19 @@ const { staffEmail, name: journalName } = config.get('journal')
const { services, Fragment } = require('pubsweet-component-helper-service')
const { getEmailCopy } = require('./emailCopy')
const { scheduleReminderJob } = require('../jobs/reminders')
const { scheduleRemovalJob } = require('../jobs/removal')
const daysList = config.get('reminders.reviewer.days')
const timeUnit = config.get('reminders.reviewer.timeUnit')
const removalDays = config.get('reminders.reviewer.remove')
const daysExpected = config.get('daysExpectedForReview')
module.exports = {
async sendReviewInvitations({
resend,
baseUrl,
fragment,
UserModel,
timestamp,
collection,
invitation,
invitedUser,
......@@ -30,7 +36,7 @@ module.exports = {
submittingAuthor,
} = await fragmentHelper.getAuthorData({ UserModel })
let subjectBaseText = `${collection.customId}: Review invitation`
const subjectBaseText = `${collection.customId}: Review invitation`
const detailsPath = `/projects/${collection.id}/versions/${
fragment.id
......@@ -70,21 +76,16 @@ module.exports = {
submittingAuthor.lastName
}`
let daysExpected = 14
let emailType = 'reviewer-invitation'
let titleText = `A manuscript titled <strong>"${title}"</strong> by <strong>${authorName}</strong> et al.`
if (resend) {
emailType = 'reviewer-resend-invitation'
daysExpected = 0
subjectBaseText = `${subjectBaseText} reminder`
titleText = `the manuscript titled "${title}" by ${authorName}`
}
const emailType = 'reviewer-invitation'
const titleText = `A manuscript titled <strong>"${title}"</strong> by <strong>${authorName}</strong> et al.`
const { paragraph, ...bodyProps } = getEmailCopy({
emailType,
titleText,
expectedDate: services.getExpectedDate({ timestamp, daysExpected }),
expectedDate: services.getExpectedDate({
timestamp: invitation.invitedOn,
daysExpected,
}),
})
const email = new Email({
......@@ -114,6 +115,32 @@ module.exports = {
bodyProps,
})
return email.sendEmail()
await email.sendEmail()
forOwn(daysList, (days, order) =>
scheduleReminderJob({
days,
order,
email,
timeUnit,
userId: invitedUser.id,
invitationId: invitation.id,
subject: `${subjectBaseText} reminder`,
titleText: `the manuscript titled "${title}" by ${authorName}`,
expectedDate: services.getExpectedDate({
timestamp: invitation.invitedOn,
daysExpected: 0,
}),
}),
)
scheduleRemovalJob({
timeUnit,
invitation,
days: removalDays,
userId: invitedUser.id,
fragmentId: fragment.id,
collectionId: collection.id,
})
},
}
const moment = require('moment')
const { cloneDeep } = require('lodash')
const Job = require('pubsweet-component-jobs')
const { getEmailCopy } = require('../emails/emailCopy')
const Email = require('@pubsweet/component-email-templating')
const scheduleReminderJob = async ({
days,
email,
order,
userId,
subject,
timeUnit,
titleText,
invitationId,
expectedDate,
}) => {
const executionDate = moment()
.add(days, timeUnit)
.toISOString()
const queue = `reminders-${userId}-${invitationId}`
const { paragraph, ...bodyProps } = getEmailCopy({
emailType: `reviewer-resend-invitation-${order}-reminder`,
titleText,
expectedDate,
})
email.bodyProps = bodyProps
email.content.subject = subject
const params = {
days,
timeUnit,
executionDate,
emailProps: cloneDeep(email),
}
await Job.schedule({ queue, params, executionDate, jobHandler })
}
const jobHandler = async job => {
const { days, timeUnit, executionDate, emailProps } = job.data
const email = new Email(emailProps)
await email.sendEmail()
return `Job ${job.id}: the ${days} ${timeUnit} reminder has been sent to ${
email.toUser.email
} at ${executionDate}`
}
module.exports = { scheduleReminderJob }
const moment = require('moment')
const { Team, Collection } = require('pubsweet-component-helper-service')
const {
Team: TeamModel,
User: UserModel,
Fragment: FragmentModel,
Collection: CollectionModel,
} = require('pubsweet-server')
const Job = require('pubsweet-component-jobs')
const scheduleRemovalJob = async ({
days,
userId,
timeUnit,
invitation,
fragmentId,
collectionId,
}) => {
const executionDate = moment()
.add(days, timeUnit)
.toISOString()
const queue = `removal-${userId}-${invitation.id}`
const params = {
days,
timeUnit,
invitation,
fragmentId,
collectionId,
executionDate,
}
await Job.schedule({ queue, executionDate, jobHandler, params })
}
const jobHandler = async job => {
const {
days,
timeUnit,
invitation,
fragmentId,
collectionId,
executionDate,
} = job.data
const collection = await CollectionModel.find(collectionId)
const fragment = await FragmentModel.find(fragmentId)
const collectionHelper = new Collection({ collection })
const teamHelper = new Team({
TeamModel,
collectionId,
fragmentId,
})
const team = await teamHelper.getTeam({
role: invitation.role,
objectType: 'fragment',
})
fragment.invitations = fragment.invitations.filter(
inv => inv.id !== invitation.id,
)
await collectionHelper.updateStatusByNumberOfReviewers({
invitations: fragment.invitations,
})
await teamHelper.removeTeamMember({
teamId: team.id,
userId: invitation.userId,
})
const user = await UserModel.find(invitation.userId)
if (!user.isConfirmed) {
await user.delete()
} else {
user.teams = user.teams.filter(userTeamId => team.id !== userTeamId)
await user.save()
}
await fragment.save()
return `Job ${
job.name
}: the ${days} ${timeUnit} removal has been executed at ${executionDate} for user ${
user.id
}`
}
module.exports = { scheduleRemovalJob }
......@@ -6,6 +6,7 @@ const {
authsome: authsomeHelper,
} = require('pubsweet-component-helper-service')
const Job = require('pubsweet-component-jobs')
const notifications = require('./emails/notifications')
module.exports = models => async (req, res) => {
......@@ -48,6 +49,9 @@ module.exports = models => async (req, res) => {
error: 'Unauthorized.',
})
await Job.cancelQueue(`reminders-${user.id}-${invitation.id}`)
await Job.cancelQueue(`removal-${user.id}-${invitation.id}`)
const collectionHelper = new Collection({ collection })
const baseUrl = services.getBaseUrl(req)
......
......@@ -7,6 +7,9 @@ const {
Fragment,
authsome: authsomeHelper,
} = require('pubsweet-component-helper-service')
const Chance = require('chance')
const chance = new Chance()
const emailInvitations = require('./emails/invitations')
......@@ -75,6 +78,10 @@ module.exports = models => async (req, res) => {
try {
const user = await UserModel.findByEmail(email)
if (user.accessTokens && !user.accessTokens.invitation) {
user.accessTokens.invitation = chance.hash()
await user.save()
}
const canInvite = await authsome.can(req.user, '', {
targetUser: user,
......@@ -89,7 +96,6 @@ module.exports = models => async (req, res) => {
let invitation = invitationHelper.getInvitation({
invitations: fragment.invitations,
})
let resend = false
if (invitation) {
if (invitation.hasAnswer) {
......@@ -97,10 +103,6 @@ module.exports = models => async (req, res) => {
.status(400)
.json({ error: 'User has already replied to a previous invitation.' })
}
invitation.invitedOn = Date.now()
await fragment.save()
resend = true
} else {
const { firstName, lastName, affiliation, country } = req.body
if (
......@@ -116,6 +118,15 @@ module.exports = models => async (req, res) => {
invitation = await invitationHelper.createInvitation({
parentObject: fragment,
})
emailInvitations.sendReviewInvitations({
baseUrl,
fragment,
collection,
invitation,
invitedUser: user,
UserModel: models.User,
})
}
const fragmentHelper = new Fragment({ fragment })
......@@ -127,17 +138,6 @@ module.exports = models => async (req, res) => {
collectionHelper.updateStatus({ newStatus: 'reviewersInvited' })
}
emailInvitations.sendReviewInvitations({
resend,
baseUrl,
fragment,
collection,
invitation,
invitedUser: user,
UserModel: models.User,
timestamp: invitation.invitedOn,
})
return res.status(200).json(invitation)
} catch (e) {
const userHelper = new User({ UserModel })
......@@ -188,7 +188,6 @@ module.exports = models => async (req, res) => {
invitation,
invitedUser: newUser,
UserModel: models.User,
timestamp: invitation.invitedOn,
})
return res.status(200).json(invitation)
......
......@@ -10,6 +10,10 @@ const cloneDeep = require('lodash/cloneDeep')
jest.mock('@pubsweet/component-send-email', () => ({
send: jest.fn(),
}))
jest.mock('pubsweet-component-jobs', () => ({
schedule: jest.fn(),
cancelQueue: jest.fn(),
}))
const reqBody = {
invitationToken: fixtures.users.reviewer.accessTokens.invitation,
......
......@@ -9,6 +9,10 @@ const { Model, fixtures } = fixturesService
jest.mock('@pubsweet/component-send-email', () => ({
send: jest.fn(),
}))
jest.mock('pubsweet-component-jobs', () => ({
schedule: jest.fn(),
cancelQueue: jest.fn(),
}))
const path = '../routes/fragmentsInvitations/delete'
const route = {
......
......@@ -10,6 +10,11 @@ jest.mock('@pubsweet/component-send-email', () => ({
send: jest.fn(),
}))
jest.mock('pubsweet-component-jobs', () => ({
schedule: jest.fn(),
cancelQueue: jest.fn(),
}))
const reqBody = {
isAccepted: true,
}
......
......@@ -11,6 +11,10 @@ const { Model, fixtures } = fixturesService
jest.mock('@pubsweet/component-send-email', () => ({
send: jest.fn(),
}))
jest.mock('pubsweet-component-jobs', () => ({
schedule: jest.fn(),
cancelQueue: jest.fn(),
}))
const chance = new Chance()
const reqBody = {
......
module.exports = require('./src/Job')
{
"name": "pubsweet-component-jobs",
"version": "0.0.1",
"description": "publishing and scheduling jobs",
"license": "MIT",
"author": "Collaborative Knowledge Foundation",
"files": [
"src"
],
"main": "index.js",
"dependencies": {
},
"peerDependencies": {
},
"publishConfig": {
"access": "public"
}
}
const { jobs: { connectToJobQueue } } = require('pubsweet-server/src')
const logger = require('@pubsweet/logger')
module.exports = {
schedule: async ({ queue, jobHandler, executionDate, params }) => {
const jobQueue = await connectToJobQueue()
// Add job to the queue
await jobQueue.publishAfter(queue, params, {}, executionDate)
// Subscribe to the job queue with an async handler
await jobQueue.subscribe(queue, jobHandler)
await jobQueue.onComplete(queue, job => {
logger.info(job.data.response.value)
})
},
cancelQueue: async name => {
const jobQueue = await connectToJobQueue()
try {
await jobQueue.unsubscribe(name)
logger.info(`Successfully unsubscribed from queue: ${name}`)
} catch (e) {
logger.error(e)
}
},
}
......@@ -3,27 +3,22 @@ import { get } from 'lodash'
import { Formik } from 'formik'
import styled from 'styled-components'
import { th } from '@pubsweet/ui-toolkit'
import { required } from 'xpub-validators'
import { H2, Menu, Button, Spinner, TextField } from '@pubsweet/ui'
import { compose, setDisplayName, withHandlers, withProps } from 'recompose'
import {
H2,
Button,
Spinner,
TextField,
ValidatedFieldFormik,
} from '@pubsweet/ui'
import {
Row,
Item,
Text,
Label,
IconButton,
MenuCountry,
RowOverrideAlert,
ItemOverrideAlert,
ValidatedMenuField,
ValidatedFormField,
withRoles,
withFetching,
withCountries,
validators,
} from 'pubsweet-component-faraday-ui'
// #region helpers
......@@ -86,16 +81,21 @@ const FormModal = ({
<Row alignItems="baseline" mb={1} mt={1}>
<ItemOverrideAlert mr={1} vertical>
<Label required>Email</Label>
<ValidatedFieldFormik
<ValidatedFormField
component={TextField}
inline
name="email"
validate={[required]}
validate={[validators.emailValidator]}
/>
</ItemOverrideAlert>
<ItemOverrideAlert ml={1} vertical>
<Label required>Role</Label>
<ValidatedMenuField name="role" options={roles} />
<ValidatedFormField
component={Menu}
name="role"
options={roles}
/>
</ItemOverrideAlert>
</Row>
)}
......@@ -103,15 +103,16 @@ const FormModal = ({
<Row mb={2}>
<Item mr={1} vertical>
<Label>First Name</Label>
<ValidatedFieldFormik
<ValidatedFormField
component={TextField}
inline
name="firstName"
/>
</Item>
<Item ml={1} vertical>
<Label>Last Name</Label>
<ValidatedFieldFormik
<ValidatedFormField
component={TextField}
inline
name="lastName"
......@@ -122,11 +123,16 @@ const FormModal = ({
<RowOverrideAlert alignItems="center" mb={2}>
<ItemOverrideAlert mr={1} vertical>
<Label>Title</Label>
<ValidatedMenuField name="title" options={titles} />
<ValidatedFormField
component={Menu}
name="title"
options={titles}
/>
</ItemOverrideAlert>
<ItemOverrideAlert ml={1} vertical>
<Label>Country</Label>
<ValidatedMenuField name="country" options={countries} />
<ValidatedFormField component={MenuCountry} name="country" />
</ItemOverrideAlert>
</RowOverrideAlert>
......@@ -134,12 +140,16 @@ const FormModal = ({
{edit && (
<ItemOverrideAlert mr={1} vertical>
<Label required>Role</Label>
<ValidatedMenuField name="role" options={roles} />
<ValidatedFormField
component={Menu}
name="role"
options={roles}
/>
</ItemOverrideAlert>
)}
<Item ml={edit && 1} vertical>
<Label>Affiliation</Label>
<ValidatedFieldFormik
<ValidatedFormField
component={TextField}
inline
name="affiliation"
......
......@@ -9,7 +9,11 @@ import { DragDropContext } from 'react-dnd'
import styled, { css } from 'styled-components'
import HTML5Backend from 'react-dnd-html5-backend'
import { Button, Spinner, Steps } from '@pubsweet/ui'
import { selectCollection, selectFragment } from 'xpub-selectors'
import {
selectFragment,
selectCollection,
selectCurrentUser,
} from 'xpub-selectors'
import { Redirect } from 'react-router'
import {
compose,
......@@ -20,7 +24,10 @@ import {
} from 'recompose'
import { Row, MultiAction, IconButton } from 'pubsweet-component-faraday-ui'
import { withModal } from 'pubsweet-component-modal/src/components'
import { getUserToken } from 'pubsweet-component-faraday-selectors/src'
import {
getUserToken,
currentUserIsAuthor,
} from 'pubsweet-component-faraday-selectors/src'
import {
reduxForm,
......@@ -51,7 +58,6 @@ import { onChange, onSubmit, setInitialValues, validate } from './utils'
const Wizard = ({
step,
history,
canEdit,
journal,
prevStep,
isEditMode,
......@@ -60,11 +66,12 @@ const Wizard = ({
handleSubmit,
getButtonText,
isFilesFetching,
canEditManuscript,
isAuthorsFetching,
journal: { manuscriptTypes = [] },
...rest
}) =>
canEdit ? (
canEditManuscript ? (
<Root className="wizard-root">
<Steps currentStep={step}>
{wizardSteps.map(({ stepTitle }) => (
......@@ -130,6 +137,11 @@ export default compose(
formValues: getFormValues('submission')(state),
submitFailed: hasSubmitFailed('submission')(state),
formSyncErrors: getFormSyncErrors('submission')(state),
isAdmin: get(selectCurrentUser(state), 'admin', false),
isAuthor: currentUserIsAuthor(
state,
get(selectFragment(state, get(match, 'params.version')), 'id', null),
),
fragment: selectFragment(state, get(match, 'params.version')),
collection: selectCollection(state, get(match, 'params.project')),
isAuthorsFetching: getAuthorFetching(state) || getAutosaveFetching(state),
......@@ -183,9 +195,11 @@ export default compose(
status: get(collection, 'status', ''),
}),
),
withProps(({ status, isLastFragment }) => ({
canEdit:
isLastFragment && !(status === 'accepted' || status === 'rejected'),
withProps(({ status, isAdmin, isAuthor, isLastFragment }) => ({
canEditManuscript:
(isAdmin || (isAuthor && status === 'draft')) &&
isLastFragment &&
!(status === 'accepted' || status === 'rejected'),
})),
withHandlers({
getButtonText: ({ isLastStep, isEditMode }) => () => {
......
......@@ -34,13 +34,10 @@ const ReviewerInviteDecision = ({
<Text align="center" secondary>
{reviewerEmail}
</Text>
<Row mt={2}>
<Text align="center">{agree === 'true' ? agreeText : declineText}</Text>
</Row>
<PasswordValidation formLabel="Password" formName="invite-reviewer" />
{fetchingError && (
<Row mt={2}>
<Text align="center" error>
......@@ -48,7 +45,6 @@ const ReviewerInviteDecision = ({
</Text>
</Row>
)}
<Row mt={2}>
{isFetching ? (
<Spinner />
......
......@@ -20,7 +20,9 @@ export default compose(
hideModal()
setSuccess(
decision.step === 'eqs'
? `Manuscript accepted. Thank you for your technical check!`
? `Manuscript ${
decision.agree ? 'accepted' : 'declined'
}. Thank you for your technical check!`
: 'Manuscript decision submitted. Thank you!',
)
})
......
......@@ -147,5 +147,17 @@ module.exports = {
passwordStrengthRegex: new RegExp(
'^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&,.?;\'*><)([}{}":`~+=_-\\|/])(?=.{6,128})',
),
reminders: {
reviewer: {
days: {
first: process.env.REMINDER_REVIEWER_FIRST || 4,
second: process.env.REMINDER_REVIEWER_SECOND || 7,
third: process.env.REMINDER_REVIEWER_THIRD || 14,
},
remove: process.env.REMINDER_REMOVE_REVIEWER || 21,
timeUnit: process.env.REMINDER_REVIEWER_TIME_UNIT || 'days',
},
},
hostname: process.env.HOSTNAME || 'http://localhost:3000',
daysExpectedForReview: 14,
}
......@@ -8168,6 +8168,11 @@ moment@^2.22.1:
version "2.22.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad"
moment@^2.23.0:
version "2.23.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225"
integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==
morgan@^1.8.2:
version "1.9.0"
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.0.tgz#d01fa6c65859b76fcf31b3cb53a3821a311d8051"
......