From 5f9c165c67aeee9a9f45253e33a47de0885a27f1 Mon Sep 17 00:00:00 2001 From: Alexandru Munteanu <alexandru.munteanu@thinslices.com> Date: Wed, 17 Jan 2018 13:49:35 +0200 Subject: [PATCH] Add drag handle to sortable list --- .../src/components/AuthorList.js | 165 ++++++++++++++++++ .../src/components/AuthorList.local.scss | 45 +++++ .../src/components/SortableList.js | 39 ++++- .../src/components/SortableList.local.scss | 5 + .../component-wizard/src/components/Wizard.js | 3 +- .../src/components/WizardStep.js | 3 + .../component-wizard/src/components/index.js | 2 + packages/xpub-faraday/app/app.js | 2 +- .../app/config/journal/manuscript-types.js | 38 +++- .../app/config/journal/submit-wizard.js | 4 + .../app/config/journal/wizard-validators.js | 14 ++ 11 files changed, 308 insertions(+), 12 deletions(-) create mode 100644 packages/component-wizard/src/components/AuthorList.js create mode 100644 packages/component-wizard/src/components/AuthorList.local.scss create mode 100644 packages/component-wizard/src/components/SortableList.local.scss create mode 100644 packages/xpub-faraday/app/config/journal/wizard-validators.js diff --git a/packages/component-wizard/src/components/AuthorList.js b/packages/component-wizard/src/components/AuthorList.js new file mode 100644 index 000000000..264be4159 --- /dev/null +++ b/packages/component-wizard/src/components/AuthorList.js @@ -0,0 +1,165 @@ +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) diff --git a/packages/component-wizard/src/components/AuthorList.local.scss b/packages/component-wizard/src/components/AuthorList.local.scss new file mode 100644 index 000000000..56e988847 --- /dev/null +++ b/packages/component-wizard/src/components/AuthorList.local.scss @@ -0,0 +1,45 @@ +.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; + } +} diff --git a/packages/component-wizard/src/components/SortableList.js b/packages/component-wizard/src/components/SortableList.js index 1273b9e12..95453bec0 100644 --- a/packages/component-wizard/src/components/SortableList.js +++ b/packages/component-wizard/src/components/SortableList.js @@ -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} diff --git a/packages/component-wizard/src/components/SortableList.local.scss b/packages/component-wizard/src/components/SortableList.local.scss new file mode 100644 index 000000000..41607ab0e --- /dev/null +++ b/packages/component-wizard/src/components/SortableList.local.scss @@ -0,0 +1,5 @@ +.drag-handle { + display: flex; + flex-direction: column; + justify-content: center; +} diff --git a/packages/component-wizard/src/components/Wizard.js b/packages/component-wizard/src/components/Wizard.js index aaf92a3d7..e13aaa0af 100644 --- a/packages/component-wizard/src/components/Wizard.js +++ b/packages/component-wizard/src/components/Wizard.js @@ -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 && ( diff --git a/packages/component-wizard/src/components/WizardStep.js b/packages/component-wizard/src/components/WizardStep.js index 672f22d3f..15af64420 100644 --- a/packages/component-wizard/src/components/WizardStep.js +++ b/packages/component-wizard/src/components/WizardStep.js @@ -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'} diff --git a/packages/component-wizard/src/components/index.js b/packages/component-wizard/src/components/index.js index 6fc7c641c..710ecfaa0 100644 --- a/packages/component-wizard/src/components/index.js +++ b/packages/component-wizard/src/components/index.js @@ -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' diff --git a/packages/xpub-faraday/app/app.js b/packages/xpub-faraday/app/app.js index fb7eae11e..6ca64de09 100644 --- a/packages/xpub-faraday/app/app.js +++ b/packages/xpub-faraday/app/app.js @@ -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 = () => { diff --git a/packages/xpub-faraday/app/config/journal/manuscript-types.js b/packages/xpub-faraday/app/config/journal/manuscript-types.js index 93488dc15..a4a62ffd6 100644 --- a/packages/xpub-faraday/app/config/journal/manuscript-types.js +++ b/packages/xpub-faraday/app/config/journal/manuscript-types.js @@ -1,42 +1,72 @@ 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, }, ] diff --git a/packages/xpub-faraday/app/config/journal/submit-wizard.js b/packages/xpub-faraday/app/config/journal/submit-wizard.js index aac92887f..9b138d17e 100644 --- a/packages/xpub-faraday/app/config/journal/submit-wizard.js +++ b/packages/xpub-faraday/app/config/journal/submit-wizard.js @@ -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], }, + {}, ], }, { diff --git a/packages/xpub-faraday/app/config/journal/wizard-validators.js b/packages/xpub-faraday/app/config/journal/wizard-validators.js new file mode 100644 index 000000000..9a9b0a9d4 --- /dev/null +++ b/packages/xpub-faraday/app/config/journal/wizard-validators.js @@ -0,0 +1,14 @@ +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 +} -- GitLab