Skip to content
Snippets Groups Projects
Commit a4110939 authored by Sam Galson's avatar Sam Galson
Browse files

Merge branch 'reviewer-update' into 'master'

feat: update reviewer suggestions form

See merge request !28
parents e4981311 e558be27
No related branches found
No related tags found
1 merge request!28feat: update reviewer suggestions form
Pipeline #6344 passed with stages
in 5 minutes and 21 seconds
import React from 'react'
import { Flex, Box } from 'grid-styled'
import { Checkbox } from '@pubsweet/ui'
import ValidatedField from '../../ui/atoms/ValidatedField'
import CalloutBox from '../../ui/atoms/CalloutBox'
import Textarea from '../../ui/atoms/Textarea'
export const SuggestedSeniorEditorRow = ({ rowIndex }) => (
<Flex>
<Box width={1 / 2}>
<ValidatedField
label="Suggested senior editor"
name={`suggestedSeniorEditors.${rowIndex}`}
/>
</Box>
<Box width={1 / 2}>
<ValidatedField
label="Suggested senior editor"
name={`suggestedSeniorEditors.${rowIndex + 1}`}
/>
</Box>
</Flex>
)
export const ExcludedSeniorEditor = ({ index }) => (
<CalloutBox>
<ValidatedField
label="Excluded senior editor"
name={`opposedSeniorEditors.${index}.name`}
/>
<ValidatedField
component={Textarea}
label="Reason for exclusion"
name={`opposedSeniorEditors.${index}.reason`}
/>
</CalloutBox>
)
export const SuggestedReviewingEditorRow = ({ rowIndex }) => (
<Flex>
<Box width={1 / 2}>
<ValidatedField
label="Suggested reviewing editor"
name={`suggestedReviewingEditors.${rowIndex}`}
/>
</Box>
<Box width={1 / 2}>
<ValidatedField
label="Suggested reviewing editor"
name={`suggestedReviewingEditors.${rowIndex + 1}`}
/>
</Box>
</Flex>
)
export const ExcludedReviewingEditor = ({ index }) => (
<CalloutBox>
<ValidatedField
label="Excluded reviewing editor"
name={`opposedReviewingEditors.${index}.name`}
/>
<ValidatedField
component={Textarea}
label="Reason for exclusion"
name={`opposedSeniorEditors.${index}.reason`}
/>
</CalloutBox>
)
export const SuggestedReviewer = ({ index }) => (
<Flex>
<Box width={1 / 2}>
<ValidatedField
label="Suggested reviewer name"
name={`suggestedReviewers.${index}.name`}
/>
</Box>
<Box width={1 / 2}>
<ValidatedField
label="Suggested reviewer email"
name={`suggestedReviewers.${index}.email`}
type="email"
/>
</Box>
</Flex>
)
export const ExcludedReviewer = ({ index }) => (
<CalloutBox>
<Flex>
<Box width={1 / 2}>
<ValidatedField
label="Excluded reviewer name"
name={`opposedReviewers.${index}.name`}
/>
</Box>
<Box width={1 / 2}>
<ValidatedField
label="Excluded reviewer email"
name={`opposedReviewers.${index}.email`}
type="email"
/>
</Box>
</Flex>
<ValidatedField
component={Textarea}
label="Reason for exclusion"
name={`opposedReviewers.${index}.reason`}
/>
</CalloutBox>
)
// pass `value` prop to `checked`
const ValueCheckbox = ({ value, validationStatus, ...props }) => (
<Box mb={3}>
<Checkbox checked={value} {...props} />
</Box>
)
export const Declaration = () => (
<Box mb={3}>
<ValidatedField
component={ValueCheckbox}
label="I declare that, to the best of my knowledge, these experts have no conflict of interest"
name="declaration"
/>
</Box>
)
import React from 'react'
import { Flex, Box } from 'grid-styled'
import { Button, H1 } from '@pubsweet/ui'
import { Box } from 'grid-styled'
import { Button, H1, PlainButton } from '@pubsweet/ui'
import ValidatedField from '../../ui/atoms/ValidatedField'
import ButtonLink from '../../ui/atoms/ButtonLink'
import ProgressBar from '../ProgressBar'
import {
Declaration,
ExcludedReviewer,
ExcludedReviewingEditor,
ExcludedSeniorEditor,
SuggestedReviewer,
SuggestedReviewingEditorRow,
SuggestedSeniorEditorRow,
} from './FormSections'
const ReviewerSuggestions = ({ handleSubmit }) => (
const MoreButton = ({
empty,
fieldName,
roleName,
setFieldValue,
type = 'suggest',
more = 'another',
values,
}) => (
<PlainButton
onClick={() =>
setFieldValue(fieldName, values[fieldName].concat(empty), false)
}
type="button"
>
{type} {values[fieldName].length ? more : 'a'} {roleName}
</PlainButton>
)
const MAX_EXCLUDED_EDITORS = 2
const ReviewerSuggestions = ({
handleSubmit,
values,
setValues,
setFieldValue,
}) => (
<form noValidate onSubmit={handleSubmit}>
<ProgressBar currentStep={3} />
<H1>Who would you like to review your work?</H1>
<ValidatedField label="Suggested editor(s)" name="suggestedEditors" />
<ValidatedField label="Excluded editor(s)" name="excludedEditors" />
<Flex>
<Box width={1 / 2}>
<ValidatedField
label="Suggested reviewer name"
name="suggestedReviewerName"
/>
</Box>
<Box width={1 / 2}>
<ValidatedField
label="Suggested reviewer email"
name="suggestedReviewerEmail"
type="email"
/>
</Box>
</Flex>
<Flex>
<Box width={1 / 2}>
<ValidatedField
label="Excluded reviewer name"
name="excludedReviewerName"
/>
</Box>
<Box width={1 / 2}>
<ValidatedField
label="Excluded reviewer email"
name="exludedReviewerEmail"
type="email"
/>
<SuggestedSeniorEditorRow rowIndex={0} />
{values.opposedSeniorEditors.map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<ExcludedSeniorEditor index={index} key={index} />
))}
{values.opposedSeniorEditors.length < MAX_EXCLUDED_EDITORS && (
<Box my={3}>
Would you like to{' '}
<MoreButton
empty={{ name: '', reason: '' }}
fieldName="opposedSeniorEditors"
roleName="senior editor"
setFieldValue={setFieldValue}
type="exclude"
values={values}
/>?
</Box>
</Flex>
)}
{values.suggestedReviewingEditors
.filter((_, index) => index % 2)
.map((_, rowIndex) => (
// eslint-disable-next-line react/no-array-index-key
<SuggestedReviewingEditorRow key={rowIndex} rowIndex={rowIndex} />
))}
{values.opposedReviewingEditors.map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<ExcludedReviewingEditor index={index} key={index} />
))}
<Box my={3}>
Would you like to{' '}
<MoreButton
empty={['', '']}
fieldName="suggestedReviewingEditors"
more="more"
roleName="reviewing editors"
setFieldValue={setFieldValue}
values={values}
/>{' '}
or{' '}
<MoreButton
empty={{ name: '', reason: '' }}
fieldName="opposedReviewingEditors"
roleName="reviewing editor"
setFieldValue={setFieldValue}
type="exclude"
values={values}
/>?
</Box>
{values.suggestedReviewers.map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<SuggestedReviewer index={index} key={index} />
))}
{values.opposedReviewers.map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<ExcludedReviewer index={index} key={index} />
))}
<Box my={3}>
Would you like to{' '}
<MoreButton
empty=""
fieldName="suggestedReviewers"
roleName="reviewer"
setFieldValue={setFieldValue}
values={values}
/>{' '}
or{' '}
<MoreButton
empty={{ name: '', email: '', reason: '' }}
fieldName="opposedReviewers"
roleName="reviewer"
setFieldValue={setFieldValue}
type="exclude"
values={values}
/>?
</Box>
<Declaration />
<Button primary type="submit">
Next
......
import React from 'react'
import { mount } from 'enzyme'
import { Formik } from 'formik'
import { MemoryRouter } from 'react-router-dom'
import ReviewerSuggestions from './ReviewerSuggestions'
import { empty, schema } from './ReviewerSuggestionsSchema'
function makeWrapper(props) {
return mount(
<MemoryRouter>
<Formik
component={ReviewerSuggestions}
initialValues={empty}
onSubmit={jest.fn()}
validationSchema={schema}
{...props}
/>
</MemoryRouter>,
)
}
function countInputsMatching(wrapper, regex) {
return wrapper
.find('input')
.filterWhere(node => node.prop('name').match(regex)).length
}
describe('ReviewerSuggestions component', () => {
it('renders default form', () => {
const wrapper = makeWrapper()
expect(countInputsMatching(wrapper, /suggestedSeniorEditor/)).toBe(2)
expect(countInputsMatching(wrapper, /suggestedReviewingEditor/)).toBe(2)
expect(countInputsMatching(wrapper, /suggestedReviewer.+name/)).toBe(3)
})
it('adds excluded senior editor', () => {
const wrapper = makeWrapper()
expect(countInputsMatching(wrapper, /opposedSeniorEditor/)).toBe(0)
wrapper
.find('MoreButton')
.at(0)
.simulate('click')
expect(countInputsMatching(wrapper, /opposedSeniorEditor/)).toBe(1)
})
})
import yup from 'yup'
const email = () => yup.string().email('Must be a valid email address')
// TODO only the initially displayed fields should be required,
// fields added by the user should be optional
const suggestedEditorValidator = () =>
yup.array(yup.string().required('Required'))
const opposedEditorValidator = () =>
yup.array(
yup.object({
name: yup.string().required('Required'),
reason: yup.string().required('Required'),
}),
)
const suggestedReviewerValidator = () =>
yup.array(
yup.object({
name: yup.string().required('Name is required'),
email: yup
.string()
.email('Must be a valid email')
.required('Email is required'),
}),
)
const opposedReviewerValidator = () =>
yup.array(
yup.object({
name: yup.string().required('Name is required'),
email: yup
.string()
.email('Must be a valid email')
.required('Email is required'),
reason: yup.string().required('Required'),
}),
)
const schema = yup.object().shape({
suggestedReviewerEmail: email(),
excludedReviewerEmail: email(),
suggestedSeniorEditors: suggestedEditorValidator(),
opposedSeniorEditors: opposedEditorValidator(),
suggestedReviewingEditors: suggestedEditorValidator(),
opposedReviewingEditors: opposedEditorValidator(),
suggestedReviewers: suggestedReviewerValidator(),
opposedReviewers: opposedReviewerValidator(),
declaration: yup
.bool()
.required()
.oneOf(
[true],
'Please do not suggest people with a known confilct of interest',
),
})
const empty = {
suggestedEditors: '',
excludedEditors: '',
suggestedReviewerName: '',
excludedReviewerName: '',
suggestedReviewerEmail: '',
excludedReviewerEmail: '',
suggestedSeniorEditors: ['', ''],
opposedSeniorEditors: [],
suggestedReviewingEditors: ['', ''],
opposedReviewingEditors: [],
suggestedReviewers: [
{ name: '', email: '' },
{ name: '', email: '' },
{ name: '', email: '' },
],
opposedReviewers: [],
declaration: false,
}
export { schema, empty }
import styled from 'styled-components'
import { Box } from 'grid-styled'
import { th } from '@pubsweet/ui'
const CalloutBox = styled(Box).attrs({ mx: -3, px: 3, mb: 3 })`
border: ${th('borderWidth')} ${th('borderStyle')} ${th('borderColor')};
border-radius: ${th('borderRadius')};
`
export default CalloutBox
import React from 'react'
import styled from 'styled-components'
import { th } from '@pubsweet/ui'
const Root = styled.div`
display: flex;
flex-direction: column;
max-width: calc(${th('gridUnit')} * 14);
margin-bottom: ${th('gridUnit')};
`
const Label = styled.label`
font-size: ${th('fontSizeBaseSmall')};
display: block;
`
const borderColor = ({ theme, validationStatus = 'default' }) =>
({
error: theme.colorError,
success: theme.colorSuccess,
warning: theme.colorWarning,
default: theme.colorBorder,
}[validationStatus])
const Input = styled.textarea`
border: ${th('borderWidth')} ${th('borderStyle')} ${borderColor};
border-radius: ${th('borderRadius')};
font-family: inherit;
font-size: inherit;
padding: calc(${th('gridUnit')} / 2);
min-height: calc(${th('fontLineHeight')} * 2);
&::placeholder {
color: ${th('colorTextPlaceholder')};
}
`
class Textarea extends React.Component {
componentWillMount() {
// generate a unique ID to link the label to the input
// note this may not play well with server rendering
this.inputId = `textarea-${Math.round(Math.random() * 1e12).toString(36)}`
}
render() {
const { label, value = '', readonly, ...props } = this.props
return (
<Root>
{label && <Label htmlFor={this.inputId}>{label}</Label>}
<Input id={this.inputId} readOnly={readonly} value={value} {...props} />
</Root>
)
}
}
export default Textarea
......@@ -15,7 +15,11 @@ const ErrorMessage = styled.div`
color: ${th('colorError')};
`
export default ({ name, component: FieldComponent = TextField, ...props }) => {
const ValidatedField = ({
name,
component: FieldComponent = TextField,
...props
}) => {
const render = ({ field, form }) => {
const touched = get(form.touched, name)
const errors = get(form.errors, name)
......@@ -42,3 +46,5 @@ export default ({ name, component: FieldComponent = TextField, ...props }) => {
return <Field name={name} render={render} />
}
export default ValidatedField
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