diff --git a/packages/component-wizard/src/components/AuthorList.js b/packages/component-wizard/src/components/AuthorList.js index 264be41596c24dddeb887612ec8b00031aef45cd..4d7f6281c87ecc90b1c92685a33dedfa1fb9b09c 100644 --- a/packages/component-wizard/src/components/AuthorList.js +++ b/packages/component-wizard/src/components/AuthorList.js @@ -1,8 +1,10 @@ import React from 'react' import { get } from 'lodash' import classnames from 'classnames' -import { TextField, Menu } from '@pubsweet/ui' +import { reduxForm } from 'redux-form' import { compose, withState, withHandlers } from 'recompose' +import { TextField, Menu, Icon, ValidatedField, Button } from '@pubsweet/ui' +import { required } from 'xpub-validators' import classes from './AuthorList.local.scss' import SortableList from './SortableList' @@ -14,48 +16,58 @@ const countries = [ { label: 'France', value: 'fr' }, ] +const emailRegex = new RegExp(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/) + +const emailValidator = value => + emailRegex.test(value) ? undefined : 'Invalid email' + +const ValidatedTextField = ({ label, name, isRequired, validators = [] }) => { + const v = [isRequired && required, ...validators].filter(Boolean) + return ( + <div className={classnames(classes['validated-text'])}> + <span className={classnames(classes.label)}>{label}</span> + <ValidatedField component={TextField} name={name} validate={v} /> + </div> + ) +} + +const MenuItem = ({ label, name, options }) => ( + <div className={classnames(classes['validated-text'])}> + <span className={classnames(classes.label)}>{label}</span> + <ValidatedField + component={input => <Menu {...input} options={options} />} + name={name} + validate={[required]} + /> + </div> +) const AuthorAdder = ({ author: { firstName, middleName, lastName, email, affiliation, country }, editAuthor, addAuthor, + handleSubmit, + ...rest }) => ( <div className={classnames(classes.adder)}> - <button onClick={addAuthor}>Add author</button> + <Button onClick={handleSubmit} primary> + + 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} - /> + <ValidatedTextField isRequired label="First name" name="firstName" /> + <ValidatedTextField label="Middle name" name="middleName" /> + <ValidatedTextField isRequired label="Last name" name="lastName" /> </div> + <div className={classnames(classes.row)}> - <TextField + <ValidatedTextField + isRequired 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} + name="email" + validators={[emailValidator]} /> + <ValidatedTextField isRequired label="Affiliation" name="affiliation" /> + <MenuItem label="Country" name="country" options={countries} /> </div> </div> ) @@ -67,89 +79,101 @@ const Label = ({ label, value }) => ( </div> ) +const DragHandle = () => ( + <div className={classnames(classes['drag-handle'])}> + <Icon>chevron_up</Icon> + <Icon size={16}>menu</Icon> + <Icon>chevron_down</Icon> + </div> +) + const Author = ({ firstName, middleName, lastName, email, affiliation, + country, isDragging, - children, + dragHandle, + isOver, + countryParser, }) => ( - <div className={classnames(classes.author)}> - <span className={classnames(classes.title)}>Author</span> - {!isDragging && ( + <div + className={classnames({ + [classes.author]: true, + [classes.dashed]: isOver, + })} + > + {!isOver && dragHandle} + <div + className={classnames({ + [classes.container]: true, + [classes.hide]: isOver, + })} + > + <span className={classnames(classes.title)}>Author</span> <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} /> + <Label label="Country" value={countryParser(country)} /> </div> - )} + </div> </div> ) -const Authors = ({ author, authors, moveAuthor, addAuthor, editAuthor }) => ( +const Adder = compose( + reduxForm({ + form: 'new-author', + onSubmit: (values, dispatch, { addAuthor, reset }) => { + addAuthor(values) + reset() + }, + })(AuthorAdder), +) + +const Authors = ({ + author, + authors, + moveAuthor, + addAuthor, + editAuthor, + ...rest +}) => ( <div> - <AuthorAdder - addAuthor={addAuthor} - author={author} - editAuthor={editAuthor} + <Adder addAuthor={addAuthor} author={author} editAuthor={editAuthor} /> + <SortableList + dragHandle={DragHandle} + items={authors} + listItem={Author} + moveItem={moveAuthor} + {...rest} /> - <SortableList items={authors} listItem={Author} moveItem={moveAuthor} /> </div> ) +const initialAuthor = { + firstName: '', + middleName: '', + lastName: '', + email: '', + affiliation: '', + country: 'ro', +} 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', - }, - ]), + withState('author', 'changeAuthor', initialAuthor), + withState('authors', 'changeAuthors', []), withHandlers({ - addAuthor: ({ author, changeAuthors, changeAuthor }) => e => { - e.preventDefault() + countryParser: () => countryCode => + countries.find(c => c.value === countryCode).label, + addAuthor: ({ changeAuthors, changeAuthor }) => author => { changeAuthors(prevAuthors => [author, ...prevAuthors]) - changeAuthor(prev => ({ - firstName: '', - middleName: '', - lastName: '', - email: '', - affiliation: '', - country: 'ro', - })) + changeAuthor(prev => initialAuthor) }, moveAuthor: ({ changeAuthors }) => (dragIndex, hoverIndex) => { changeAuthors(prev => SortableList.moveItem(prev, dragIndex, hoverIndex)) diff --git a/packages/component-wizard/src/components/AuthorList.local.scss b/packages/component-wizard/src/components/AuthorList.local.scss index 56e988847b48eb9bca459c8780897358f2a4ea4b..bfa0dcc4774c8d496dfa59f1fca31e5fd5702bc6 100644 --- a/packages/component-wizard/src/components/AuthorList.local.scss +++ b/packages/component-wizard/src/components/AuthorList.local.scss @@ -1,10 +1,32 @@ .row { display: flex; flex-direction: row; + margin: 10px 0; +} + +.hide { + opacity: 0; +} + +.dashed { + border: 1px dashed #444 !important; +} + +.label { + font-size: 14px; + font-weight: 300; + text-transform: uppercase; } .author { border: 1px solid #444; + display: flex; + flex-direction: row; + margin-bottom: 10px; + + .container { + flex: 1; + } .title { font-size: 16px; @@ -18,12 +40,6 @@ flex-direction: column; margin: 5px; - .label { - font-size: 14px; - font-weight: 300; - text-transform: uppercase; - } - .value { font-size: 16px; font-weight: 600; @@ -32,14 +48,50 @@ } .adder { - background-color: aquamarine; + border: 1px solid #444; display: flex; flex-direction: column; margin: 10px 0; - padding: 5px; + padding: 10px; + + .button { + background-color: #444; + border: none; + color: #eee; + cursor: pointer; + font-size: 14px; + font-weight: 500; + height: 24px; + text-transform: uppercase; + + &:hover { + background-color: #666; + } + + &:focus, + &:active { + outline: none; + } + } .title { font-size: 18px; font-weight: 500; + margin: 10px 0; + text-transform: uppercase; } } + +.drag-handle { + align-items: center; + border-right: 1px solid #444; + cursor: move; + display: flex; + flex-direction: column; + justify-content: center; +} + +.validated-text { + flex: 1; + margin-right: 20px; +} diff --git a/packages/component-wizard/src/components/SortableList.js b/packages/component-wizard/src/components/SortableList.js index 95453bec0f48eeadffd7157c433c5d63821bda35..e7db078bf3b279bd486d425cf647f9d076c537a5 100644 --- a/packages/component-wizard/src/components/SortableList.js +++ b/packages/component-wizard/src/components/SortableList.js @@ -2,9 +2,6 @@ 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) { @@ -41,33 +38,34 @@ const itemTarget = { }, } -const DragHandle = () => ( - <div className={classnames(classes['drag-handle'])}> - <Icon>chevron_up</Icon> - <Icon>chevron_down</Icon> - </div> -) - const Item = ({ connectDragPreview, connectDragSource, connectDropTarget, listItem, + dragHandle, ...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>, - ) + dragHandle + ? connectDragPreview( + connectDropTarget( + <div style={{ flex: 1 }}> + {React.createElement(listItem, { + ...rest, + dragHandle: connectDragSource( + <div style={{ display: 'flex' }}> + {React.createElement(dragHandle)} + </div>, + ), + })} + </div>, + ), + ) + : connectDropTarget( + connectDragSource( + <div style={{ flex: 1 }}>{React.createElement(listItem, rest)}</div>, + ), + ) const DecoratedItem = compose( DropTarget('item', itemTarget, (connect, monitor) => ({ @@ -81,15 +79,17 @@ const DecoratedItem = compose( })), )(Item) -const SortableList = ({ items, moveItem, listItem }) => ( +const SortableList = ({ items, moveItem, listItem, dragHandle, ...rest }) => ( <div> {items.map((item, i) => ( <DecoratedItem + dragHandle={dragHandle} index={i} key={item.name || Math.random()} listItem={listItem} moveItem={moveItem} {...item} + {...rest} /> ))} </div> diff --git a/packages/component-wizard/src/components/SortableList.local.scss b/packages/component-wizard/src/components/SortableList.local.scss deleted file mode 100644 index 41607ab0e84369690e3e1d24b2e2dccd241c0dda..0000000000000000000000000000000000000000 --- a/packages/component-wizard/src/components/SortableList.local.scss +++ /dev/null @@ -1,5 +0,0 @@ -.drag-handle { - display: flex; - flex-direction: column; - justify-content: center; -} diff --git a/packages/component-wizard/src/components/WizardStep.js b/packages/component-wizard/src/components/WizardStep.js index 15af64420572a6286a5cad303624c1e4d64245f8..672f22d3fca3f1c9f28e85b3accd87251f04e636 100644 --- a/packages/component-wizard/src/components/WizardStep.js +++ b/packages/component-wizard/src/components/WizardStep.js @@ -5,8 +5,6 @@ import { ValidatedField, Button } from '@pubsweet/ui' import classes from './WizardStep.local.scss' -import AuthorList from './AuthorList' - export default ({ children: stepChildren, title, @@ -47,7 +45,6 @@ export default ({ ) }, )} - <AuthorList /> <div className={classnames(classes.buttons)}> <Button onClick={isFirst ? goBack : prevStep}> {isFirst ? 'Cancel' : 'Back'} diff --git a/packages/xpub-faraday/app/config/journal/submit-wizard.js b/packages/xpub-faraday/app/config/journal/submit-wizard.js index 9b138d17e0b2e4df06029f2f60ccd7221975ad4c..73318a55ec5ed0f135e8c0b247f321f96622d35a 100644 --- a/packages/xpub-faraday/app/config/journal/submit-wizard.js +++ b/packages/xpub-faraday/app/config/journal/submit-wizard.js @@ -9,6 +9,7 @@ import { } from '@pubsweet/ui' import uploadFile from 'xpub-upload' import { required, minChars, minSize } from 'xpub-validators' +import { AuthorList } from 'pubsweet-component-wizard/src/components' import { declarations } from './' import issueTypes from './issues-types' @@ -98,6 +99,10 @@ export default { placeholder: 'Write an abstract', validate: [requiredBasedOnType], }, + { + fieldId: 'authors', + renderComponent: AuthorList, + }, { fieldId: 'conflicts.hasConflicts', renderComponent: yesNoWithLabel, @@ -113,7 +118,6 @@ export default { label: 'Conflict of interest details', validate: [required, min3Chars], }, - {}, ], }, {