Skip to content
Snippets Groups Projects
Commit 5f9c165c authored by Alexandru Munteanu's avatar Alexandru Munteanu
Browse files

Add drag handle to sortable list

parent df98ebbc
No related branches found
No related tags found
No related merge requests found
Showing
with 308 additions and 12 deletions
import React from 'react'
import { get } from 'lodash'
import classnames from 'classnames'
import { TextField, Menu } from '@pubsweet/ui'
import { compose, withState, withHandlers } from 'recompose'
import classes from './AuthorList.local.scss'
import SortableList from './SortableList'
const countries = [
{ label: 'Romania', value: 'ro' },
{ label: 'United Kingdom', value: 'uk' },
{ label: 'Germany', value: 'de' },
{ label: 'France', value: 'fr' },
]
const AuthorAdder = ({
author: { firstName, middleName, lastName, email, affiliation, country },
editAuthor,
addAuthor,
}) => (
<div className={classnames(classes.adder)}>
<button onClick={addAuthor}>Add author</button>
<span className={classnames(classes.title)}>Author</span>
<div className={classnames(classes.row)}>
<TextField
label="First name"
onChange={editAuthor('firstName')}
value={firstName}
/>
<TextField
label="Midle name"
onChange={editAuthor('middleName')}
value={middleName}
/>
<TextField
label="Last name"
onChange={editAuthor('lastName')}
value={lastName}
/>
</div>
<div className={classnames(classes.row)}>
<TextField
label="Email"
onChange={editAuthor('email')}
type="email"
value={email}
/>
<TextField
label="Affiliation"
onChange={editAuthor('affiliation')}
value={affiliation}
/>
<Menu
onChange={editAuthor('country')}
options={countries}
value={country}
/>
</div>
</div>
)
const Label = ({ label, value }) => (
<div className={classnames(classes['label-container'])}>
<span className={classnames(classes.label)}>{label}</span>
<span className={classnames(classes.value)}>{value}</span>
</div>
)
const Author = ({
firstName,
middleName,
lastName,
email,
affiliation,
isDragging,
children,
}) => (
<div className={classnames(classes.author)}>
<span className={classnames(classes.title)}>Author</span>
{!isDragging && (
<div className={classnames(classes.row)}>
<Label label="First name" value={firstName} />
<Label label="Middle name" value={middleName} />
<Label label="Last name" value={lastName} />
</div>
)}
{!isDragging && (
<div className={classnames(classes.row)}>
<Label label="Email" value={email} />
<Label label="Affiliation" value={affiliation} />
<Label label="Affiliation" value={affiliation} />
</div>
)}
</div>
)
const Authors = ({ author, authors, moveAuthor, addAuthor, editAuthor }) => (
<div>
<AuthorAdder
addAuthor={addAuthor}
author={author}
editAuthor={editAuthor}
/>
<SortableList items={authors} listItem={Author} moveItem={moveAuthor} />
</div>
)
export default compose(
withState('author', 'changeAuthor', {
firstName: '',
middleName: '',
lastName: '',
email: '',
affiliation: '',
country: 'ro',
}),
withState('authors', 'changeAuthors', [
{
firstName: 'Razvan',
middleName: 'Petru',
lastName: 'Tudosa',
email: 'rzv@gmail.com',
affiliation: 'rock',
},
{
firstName: 'Alexandru',
middleName: 'Ioan',
lastName: 'Munteanu',
email: 'alexmntn@gmail.com',
affiliation: 'rap',
},
{
firstName: 'Bogdan',
middleName: 'Alexandru',
lastName: 'Cochior',
email: 'bog1@gmail.com',
affiliation: 'metal',
},
]),
withHandlers({
addAuthor: ({ author, changeAuthors, changeAuthor }) => e => {
e.preventDefault()
changeAuthors(prevAuthors => [author, ...prevAuthors])
changeAuthor(prev => ({
firstName: '',
middleName: '',
lastName: '',
email: '',
affiliation: '',
country: 'ro',
}))
},
moveAuthor: ({ changeAuthors }) => (dragIndex, hoverIndex) => {
changeAuthors(prev => SortableList.moveItem(prev, dragIndex, hoverIndex))
},
editAuthor: ({ changeAuthor }) => field => e => {
const v = get(e, 'target.value') || e
changeAuthor(prev => ({
...prev,
[field]: v,
}))
},
}),
)(Authors)
.row {
display: flex;
flex-direction: row;
}
.author {
border: 1px solid #444;
.title {
font-size: 16px;
font-weight: 600;
margin: 5px;
}
.label-container {
display: flex;
flex: 1;
flex-direction: column;
margin: 5px;
.label {
font-size: 14px;
font-weight: 300;
text-transform: uppercase;
}
.value {
font-size: 16px;
font-weight: 600;
}
}
}
.adder {
background-color: aquamarine;
display: flex;
flex-direction: column;
margin: 10px 0;
padding: 5px;
.title {
font-size: 18px;
font-weight: 500;
}
}
......@@ -2,6 +2,9 @@ import React from 'react'
import { compose } from 'recompose'
import { findDOMNode } from 'react-dom'
import { DragSource, DropTarget } from 'react-dnd'
import classnames from 'classnames'
import { Icon } from '@pubsweet/ui'
import classes from './SortableList.local.scss'
const itemSource = {
beginDrag(props) {
......@@ -31,14 +34,39 @@ const itemTarget = {
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return
}
moveItem(dragIndex, hoverIndex)
if (typeof moveItem === 'function') {
moveItem(dragIndex, hoverIndex)
}
monitor.getItem().index = hoverIndex
},
}
const Item = ({ connectDragSource, connectDropTarget, listItem, ...rest }) =>
connectDragSource(
connectDropTarget(<div>{React.createElement(listItem, rest)}</div>),
const DragHandle = () => (
<div className={classnames(classes['drag-handle'])}>
<Icon>chevron_up</Icon>
<Icon>chevron_down</Icon>
</div>
)
const Item = ({
connectDragPreview,
connectDragSource,
connectDropTarget,
listItem,
...rest
}) =>
connectDragPreview(
<div style={{ display: 'flex' }}>
{connectDragSource(
<div className={classnames(classes['drag-handle'])}>
<Icon>chevron_up</Icon>
<Icon>chevron_down</Icon>
</div>,
)}
{connectDropTarget(
<div style={{ flex: 1 }}>{React.createElement(listItem, rest)}</div>,
)}
</div>,
)
const DecoratedItem = compose(
......@@ -48,6 +76,7 @@ const DecoratedItem = compose(
})),
DragSource('item', itemSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging(),
})),
)(Item)
......@@ -57,7 +86,7 @@ const SortableList = ({ items, moveItem, listItem }) => (
{items.map((item, i) => (
<DecoratedItem
index={i}
key={item.name}
key={item.name || Math.random()}
listItem={listItem}
moveItem={moveItem}
{...item}
......
.drag-handle {
display: flex;
flex-direction: column;
justify-content: center;
}
......@@ -10,10 +10,9 @@ const { Step } = Progress
export default ({
journal: { wizard: { showProgress, steps } },
getSteps,
step,
nextStep,
prevStep,
...rest
step,
}) => (
<div className={classnames(classes.container)}>
{showProgress && (
......
......@@ -5,6 +5,8 @@ import { ValidatedField, Button } from '@pubsweet/ui'
import classes from './WizardStep.local.scss'
import AuthorList from './AuthorList'
export default ({
children: stepChildren,
title,
......@@ -45,6 +47,7 @@ export default ({
)
},
)}
<AuthorList />
<div className={classnames(classes.buttons)}>
<Button onClick={isFirst ? goBack : prevStep}>
{isFirst ? 'Cancel' : 'Back'}
......
......@@ -5,3 +5,5 @@ export { default as WizardPage } from './WizardPage'
export { default as WizardStep } from './WizardStep'
export { default as SortableList } from './SortableList'
export { default as WizardFormStep } from './WizardFormStep'
export { default as AuthorList } from './AuthorList'
......@@ -11,7 +11,7 @@ import * as journal from './config/journal'
import Routes from './routes'
const history = createHistory()
const store = configureStore(history, {})
export const store = configureStore(history, {})
const theme = {}
const render = () => {
......
export default [
{ label: 'Research', value: 'research', author: true, peerReview: true },
{ label: 'Review', value: 'review', author: true, peerReview: true },
{
label: 'Research',
value: 'research',
author: true,
peerReview: true,
abstractRequired: true,
},
{
label: 'Review',
value: 'review',
author: true,
peerReview: true,
abstractRequired: true,
},
{
label: 'Clinical study',
value: 'clinical-study',
author: true,
peerReview: true,
abstractRequired: true,
},
{
label: 'Case report',
value: 'case-report',
author: true,
peerReview: true,
abstractRequired: true,
},
{
label: 'Letter to the editor',
value: 'letter-to-editor',
author: true,
peerReview: false,
abstractRequired: false,
},
{
label: 'Editorial',
value: 'editorial',
author: false,
peerReview: false,
abstractRequired: false,
},
{ label: 'Editorial', value: 'editorial', author: false, peerReview: false },
{
label: 'Corrigendum',
value: 'corrigendum',
author: false,
peerReview: false,
abstractRequired: false,
},
{
label: 'Erratum',
value: 'erratum',
author: false,
peerReview: false,
abstractRequired: false,
},
{ label: 'Erratum', value: 'erratum', author: false, peerReview: false },
{
label: 'Expression of concern',
value: 'expression-of-concern',
author: false,
peerReview: false,
abstractRequired: true,
},
{
label: 'Retraction',
value: 'retraction',
author: false,
peerReview: false,
abstractRequired: false,
},
]
......@@ -14,6 +14,8 @@ import { declarations } from './'
import issueTypes from './issues-types'
import manuscriptTypes from './manuscript-types'
import { requiredBasedOnType } from './wizard-validators'
const min3Chars = minChars(3)
const declarationsMinSize = minSize(declarations.options.length)
......@@ -94,6 +96,7 @@ export default {
renderComponent: AbstractEditor,
title: 'Abstract',
placeholder: 'Write an abstract',
validate: [requiredBasedOnType],
},
{
fieldId: 'conflicts.hasConflicts',
......@@ -110,6 +113,7 @@ export default {
label: 'Conflict of interest details',
validate: [required, min3Chars],
},
{},
],
},
{
......
import { get } from 'lodash'
import manuscriptTypes from './manuscript-types'
const requiredTypes = manuscriptTypes
.filter(t => t.abstractRequired)
.map(t => t.value)
export const requiredBasedOnType = (value, formValues) => {
if (requiredTypes.includes(get(formValues, 'metadata.type'))) {
return 'Required'
}
return undefined
}
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