From 5822b2ca5a497ce039a32f7f6c8f1adfffe374df Mon Sep 17 00:00:00 2001 From: ahamelers <audrey@ahamelers.com> Date: Sun, 8 Jul 2018 16:53:44 +0100 Subject: [PATCH] fix: #98 --- app/components/Create.jsx | 20 +- app/components/Dashboard.jsx | 7 +- app/components/DashboardPage.jsx | 36 +- app/components/PubMedSearch.jsx | 202 +++++------ app/components/PubMedSearchResult.jsx | 6 + app/components/Submit.jsx | 25 +- app/components/UnmatchedCitation.jsx | 340 +++++++++---------- app/components/ui/atoms/Buttons.jsx | 7 +- app/components/ui/atoms/Page.jsx | 16 +- app/components/ui/atoms/TextArea.jsx | 68 ++++ app/components/ui/atoms/index.js | 3 +- app/components/ui/molecules/SearchSelect.jsx | 18 +- app/redux/createsubmission.js | 18 +- app/routes.js | 2 +- server/eutils/api.js | 4 +- 15 files changed, 438 insertions(+), 334 deletions(-) create mode 100644 app/components/ui/atoms/TextArea.jsx diff --git a/app/components/Create.jsx b/app/components/Create.jsx index 3ef8e475b..95a8f0c1a 100644 --- a/app/components/Create.jsx +++ b/app/components/Create.jsx @@ -2,7 +2,7 @@ import React from 'react' import { withRouter } from 'react-router-dom' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' -import { Icon, Button, ErrorText, H2, H3 } from '@pubsweet/ui' +import { Icon, Button, ErrorText, H2 } from '@pubsweet/ui' import { updateStatus, createNewManuscriptVersion, @@ -137,17 +137,21 @@ class EPMCCreated extends React.Component { <CreatePageHeader currentStep={currentStep} /> {currentStep === 0 && ( <div> - <H2> - <Icon size={4}>check</Icon>Citation selected - </H2> - <Citation metadata={metadata} /> - {this.state.showSearch && ( + {this.state.showSearch ? ( <div> - <H3>Change your citation</H3> + <H2>Citation</H2> <PubMedSearch citationData={this.createNewManuscriptVersion} + metadata={metadata} /> </div> + ) : ( + <div> + <H2> + Citation selected<Icon size={4}>check</Icon> + </H2> + <Citation metadata={metadata} /> + </div> )} </div> )} @@ -189,7 +193,6 @@ class EPMCCreated extends React.Component { /> )} <Buttons> - <Status>{status}</Status> <div> {!this.state.showSearch && ( <Button onClick={this.goPrev}>Back</Button> @@ -201,6 +204,7 @@ class EPMCCreated extends React.Component { Next </Button> </div> + <Status>{status}</Status> </Buttons> </div> </StepPanel> diff --git a/app/components/Dashboard.jsx b/app/components/Dashboard.jsx index c36703350..a9aebb2a3 100644 --- a/app/components/Dashboard.jsx +++ b/app/components/Dashboard.jsx @@ -52,11 +52,8 @@ export default class DashSwitch extends React.Component { <StepPanel> <div> <CreateHeader currentStep={0} /> - <H2>Citation information</H2> + <H2>Citation</H2> <PubMedSearch citationData={createManuscript} /> - <Button onClick={() => this.setState({ showButton: false })}> - Cancel - </Button> </div> </StepPanel> <InfoPanel> @@ -69,7 +66,7 @@ export default class DashSwitch extends React.Component { <Page> <UploadContainer> <BigLink onClick={() => this.setState({ showButton: true })} primary> - Create New Submission + Submit a new manuscript </BigLink> </UploadContainer> diff --git a/app/components/DashboardPage.jsx b/app/components/DashboardPage.jsx index 85f067975..e48fb826f 100644 --- a/app/components/DashboardPage.jsx +++ b/app/components/DashboardPage.jsx @@ -19,25 +19,35 @@ const reviewerResponse = (project, version, reviewer, status) => dispatch => { } const createManuscript = (citationData, history) => dispatch => { - const { title } = citationData + const { title, specialInstructions, ...other } = citationData dispatch(actions.createCollection({ title })).then(({ collection }) => { if (!collection.id) { throw new Error('Failed to create a project') } // TODO: create teams? // TODO: rethrow errors so they can be caught here - // TODO: change way ID is generated - return dispatch( - actions.createFragment(collection, { - created: new Date(), // TODO: set on server - fragmentType: 'version', - metadata: citationData, - version: 1, - }), - ).then(({ fragment }) => { - const route = `/projects/${collection.id}/versions/${fragment.id}/create` - history.push(route) - }) + const data = { + created: new Date(), // TODO: set on server + fragmentType: 'version', + metadata: { + title, + ...other, + }, + version: 1, + } + if (specialInstructions) { + data.notes = { + specialInstructions, + } + } + return dispatch(actions.createFragment(collection, data)).then( + ({ fragment }) => { + const route = `/projects/${collection.id}/versions/${ + fragment.id + }/create` + history.push(route) + }, + ) }) } diff --git a/app/components/PubMedSearch.jsx b/app/components/PubMedSearch.jsx index d9481ea29..cbcf71c29 100644 --- a/app/components/PubMedSearch.jsx +++ b/app/components/PubMedSearch.jsx @@ -1,9 +1,21 @@ import React from 'react' +import ReactDOM from 'react-dom' import { withRouter } from 'react-router-dom' import styled from 'styled-components' import { th } from '@pubsweet/ui-toolkit' import { Button, Icon, H2 } from '@pubsweet/ui' -import { A, Buttons, IconButton, Loading, LoadingIcon, SearchForm } from './ui/' +import { + A, + B, + Buttons, + Close, + CloseIcon, + Cover, + Loading, + LoadingIcon, + Page, + SearchForm, +} from './ui/' import PubMedSearchResult from './PubMedSearchResult' import UnmatchedCitation from './UnmatchedCitation' @@ -13,11 +25,16 @@ const LoadMore = styled(Button)` const SearchArea = styled.div` margin: 0 auto; ` +const Notice = styled.p` + margin: 0 auto calc(${th('gridUnit')} * 3); + a { + display: flex; + align-items: center; + justify-content: flex-start; + } +` const Results = styled.div` - max-height: 40vh; - overflow: auto; text-align: center; - border: ${th('borderWidth')} ${th('borderStyle')} ${th('colorBackgroundHue')}; ` class PubMedSearch extends React.Component { constructor(props) { @@ -36,28 +53,10 @@ class PubMedSearch extends React.Component { this.onLoad = this.onLoad.bind(this) this.onQueryChange = this.onQueryChange.bind(this) this.onSelected = this.onSelected.bind(this) + this.handleUnmatchedInfo = this.handleUnmatchedInfo.bind(this) } - handleUnmatchedInfo = unmatchedCitationInfo => { - if (unmatchedCitationInfo.nlmId) { - const metadata = { - title: unmatchedCitationInfo.title, - journalMeta: { - nlmuniqueid: unmatchedCitationInfo.nlmId - ? unmatchedCitationInfo.nlmId - : '', - title: unmatchedCitationInfo.journal, - }, - } - this.props.citationData(metadata) - } else { - const metadata = { - title: unmatchedCitationInfo.title, - customMeta: { - unmatchedJournal: unmatchedCitationInfo.journal, - }, - } - this.props.citationData(metadata) - } + handleUnmatchedInfo(metadata) { + this.props.citationData(metadata) } onQueryChange(event) { this.setState({ @@ -149,6 +148,27 @@ class PubMedSearch extends React.Component { } } render() { + const { metadata } = this.props + const { title, journalMeta } = metadata || {} + const { title: journalTitle } = journalMeta || {} + + if (this.state.unmatched) { + return ReactDOM.createPortal( + <Cover> + <Page> + <Close> + <CloseIcon onClick={() => this.setState({ unmatched: false })} /> + </Close> + <UnmatchedCitation + journal={title} + title={journalTitle} + unmatchedInfo={this.handleUnmatchedInfo} + /> + </Page> + </Cover>, + document.getElementById('root').firstChild, + ) + } return ( <SearchArea> {this.state.inPMC ? ( @@ -163,7 +183,7 @@ class PubMedSearch extends React.Component { <Button onClick={() => window.location.reload()} primary> End Submission </Button> - <IconButton + <Button onClick={() => this.setState({ inPMC: null, @@ -171,85 +191,75 @@ class PubMedSearch extends React.Component { }) } > - <Icon>chevron_left</Icon>Back to Search - </IconButton> + Back to Search + </Button> </Buttons> </div> ) : ( <div> - {this.state.unmatched ? ( - <div> - <UnmatchedCitation - getUnmatchedInfo={this.handleUnmatchedInfo} - /> - <p> - <IconButton - onClick={() => this.setState({ unmatched: false })} - > - <Icon>chevron_left</Icon>Back to Search - </IconButton> - </p> - </div> - ) : ( - <div> - <SearchForm - buttonLabel="Search" - disabled={!this.state.enabled} - label="Search for your article" - name="Search" - onChange={this.onQueryChange} - onSubmit={this.onSearch} - placeholder="Your query" - value={this.state.query} - /> - {this.state.results.length > 0 && ( - <Results> - {this.state.results.map( - result => - result.fulljournalname && ( - <PubMedSearchResult - key={result.uid} - onClick={() => this.onSelected(result)} - result={result} - /> - ), - )} - {this.state.loading && ( - <Loading> - <LoadingIcon /> - </Loading> - )} - {this.state.results.length < this.state.hitcount && - !this.state.loading && ( - <LoadMore onClick={this.onLoad} secondary> - Load More Results - </LoadMore> - )} - </Results> - )} - {this.state.results.length === 0 && - this.state.loading && ( - <Loading> - <LoadingIcon /> - </Loading> - )} + <SearchForm + buttonLabel="Search" + disabled={!this.state.enabled} + label="Search for your article" + name="Search" + onChange={this.onQueryChange} + onSubmit={this.onSearch} + placeholder="Your query" + value={this.state.query} + /> + {this.state.hitcount !== null && ( + <Notice> + <B>Select your citation from results</B> + <br /> {this.state.hitcount === 0 && ( - <p> - <A onClick={() => this.setState({ unmatched: true })}> - No results found. Click to enter citation manually. - </A> - </p> + <A onClick={() => this.setState({ unmatched: true })}> + <Icon color="currentColor" size={2}> + info + </Icon>{' '} + No results found. Click to enter citation manually. + </A> )} {this.state.results.length > 0 && ( - <p> - <A onClick={() => this.setState({ unmatched: true })}> - Manuscript not in results? Click to enter citation - manually. - </A> - </p> + <A onClick={() => this.setState({ unmatched: true })}> + <Icon color="currentColor" size={2}> + info + </Icon>{' '} + Manuscript not in results? Click to enter citation manually. + </A> + )} + </Notice> + )} + {this.state.results.length > 0 && ( + <Results> + {this.state.results.map( + result => + result.fulljournalname && ( + <PubMedSearchResult + key={result.uid} + onClick={() => this.onSelected(result)} + result={result} + /> + ), )} - </div> + {this.state.loading && ( + <Loading> + <LoadingIcon /> + </Loading> + )} + {this.state.results.length < this.state.hitcount && + !this.state.loading && ( + <LoadMore onClick={this.onLoad} secondary> + Load More Results + </LoadMore> + )} + </Results> )} + {this.state.results.length === 0 && + this.state.loading && ( + <Loading> + <LoadingIcon /> + </Loading> + )} </div> )} </SearchArea> diff --git a/app/components/PubMedSearchResult.jsx b/app/components/PubMedSearchResult.jsx index 70fdb1ddd..4a2808396 100644 --- a/app/components/PubMedSearchResult.jsx +++ b/app/components/PubMedSearchResult.jsx @@ -6,9 +6,15 @@ import { HTMLString } from './ui/' const ResultContainer = styled.div` padding: calc(${th('gridUnit')} * 2) ${th('gridUnit')}; text-align: left; + background-color: ${th('colorTextReverse')}; + border: ${th('borderWidth')} ${th('borderStyle')} ${th('colorBorder')}; &:nth-child(odd) { background-color: ${th('colorBackgroundHue')}; } + &:nth-child(even) { + border-top: 0; + border-bottom: 0; + } &:hover { cursor: pointer; color: ${th('colorPrimary')}; diff --git a/app/components/Submit.jsx b/app/components/Submit.jsx index 7e01b5027..2556c8eb9 100644 --- a/app/components/Submit.jsx +++ b/app/components/Submit.jsx @@ -5,7 +5,7 @@ import styled, { css, withTheme } from 'styled-components' import { th } from '@pubsweet/ui-toolkit' import { Icon, ErrorText, H2, H3 } from '@pubsweet/ui' -import { Page, Close, CloseIcon, A } from './ui/' +import { Page, Center, Cover, Close, CloseIcon, A } from './ui/' import { updateStatus, createNewManuscriptVersion, @@ -86,17 +86,6 @@ const Header = styled.div` margin-bottom: 0px; } ` -const Cover = styled.div` - position: fixed; - right: 0; - left: 0; - top: 0; - bottom: 0; - width: 100%; - height: 100%; - background-color: #fff; - text-align: center; -` const Image = styled.img` max-width: 100%; ` @@ -389,11 +378,13 @@ class EPMCSubmit extends React.Component { onClick={() => this.setState({ showFigure: null })} /> </Close> - <Image src={showFigure.url} /> - <p> - <b>{showFigure.label ? `${showFigure.label}:` : ''}</b> - {showFigure.name} - </p> + <Center> + <Image src={showFigure.url} /> + <p> + <b>{showFigure.label ? `${showFigure.label}:` : ''}</b> + {showFigure.name} + </p> + </Center> </Page> </Cover>, document.getElementById('root').firstChild, diff --git a/app/components/UnmatchedCitation.jsx b/app/components/UnmatchedCitation.jsx index fc847a1bf..97d6bbc82 100644 --- a/app/components/UnmatchedCitation.jsx +++ b/app/components/UnmatchedCitation.jsx @@ -1,212 +1,198 @@ import React from 'react' -import styled from 'styled-components' -import { th } from '@pubsweet/ui-toolkit' -import { ErrorText, TextField } from '@pubsweet/ui' -import { HTMLString, Loading, LoadingIcon, SearchForm } from './ui/' - -const ResultList = styled.ul` - list-style-type: none; - margin: 0; - padding: 0; - max-height: calc(${th('gridUnit')} * 42); - overflow: auto; - text-align: left; - border: ${th('borderWidth')} ${th('borderStyle')} ${th('colorBackgroundHue')}; -` -const Input = styled.div` - width: 100%; - & > div:first-child { - width: 100%; - max-width: 100%; - } -` -const ResultListItem = styled.li` - display: flex; - align-items: center; - padding: 0 ${th('gridUnit')}; - min-height: calc(${th('gridUnit')} * 6); - &:hover { - cursor: pointer; - color: ${th('colorPrimary')}; - } - &:nth-child(odd) { - background-color: ${th('colorBackgroundHue')}; - } -` +import * as Joi from 'joi' +import { debounce } from 'lodash' +import { Button, ErrorText, H2, TextField } from '@pubsweet/ui' +import { Buttons, HTMLString, SearchSelect, TextArea } from './ui/' export default class UnmatchedCitation extends React.Component { constructor(props) { super(props) this.state = { title: '', - journal: '', - journals: [], - ids: [], - // id: null, - doSubmit: false, + query: '', + journal: null, + note: this.props.note, enabled: false, - loading: false, + journals: [], titleMessage: '', - journalNotExistsMessage: '', + journalMessage: '', } - - this.onSearchForJournal = this.onSearchForJournal.bind(this) this.onQueryChange = this.onQueryChange.bind(this) + this.onSearch = debounce(this.onSearch.bind(this), 100) + this.enable = this.enable.bind(this) + this.submitData = this.submitData.bind(this) + this.onTitleChange = this.onTitleChange.bind(this) + this.onJournalChange = this.onJournalChange.bind(this) } - - async onSearchForJournal(event) { - event.preventDefault() - - this.setState({ - enabled: false, - loading: true, - journalNotExistsMessage: '', - }) - if (this.state.doSubmit) { - if (this.state.title && this.state.journal) { - this.props.getUnmatchedInfo({ - title: this.state.title, - journal: this.state.journal, - }) - } else { - this.setState({ titleMessage: 'Title is required.' }) - } + enable() { + const validTitle = Joi.validate(this.state.title, Joi.string().required()) + const validJournal = Joi.validate( + this.state.journal, + Joi.object({ + title: Joi.string().required(), + nlmuniqueid: Joi.string(), + }), + ) + if (!validTitle.error && !validJournal.error) { + this.setState({ enabled: true }) } else { this.setState({ - journals: [], - ids: [], - // id: null, - doSubmit: false, + titleMessage: validTitle.error ? 'Title is required.' : '', + journalMessage: validJournal.error ? 'Journal is required.' : '', }) - - const query1 = `/eutils/esearch?term=%22${ - this.state.journal - }%22[Title]%20AND%20(serial[Item%20Type]%20AND%20(all[subset]%20NOT%20none[URL]))&db=nlmcatalog&retstart=0` - const response = await fetch(query1, { - headers: new Headers({ - Authorization: `Bearer ${window.localStorage.getItem('token')}`, - }), - }) - const json = await response.json() - const ids = json.esearchresult.idlist - const hitcount = json.esearchresult.count - - if (hitcount > 0) { - const query2 = `/eutils/efetch?db=nlmcatalog&id=${ids.join()}` - const summary = await fetch(query2, { - headers: new Headers({ - Authorization: `Bearer ${window.localStorage.getItem('token')}`, - }), - }) - const xml = await summary.text() - - const titles = xml.match(/<Title(.*)>(.*)<\/Title>/g) - const nlmIds = xml.match(/<NlmUniqueID>(.*)<\/NlmUniqueID>/g) - const journals = titles - ? titles.map(x => x.match(/<Title(.*)>(.*)<\/Title>/)[2]) - : [] - this.setState({ - journals, - ids: nlmIds - ? nlmIds.map(x => x.match(/<NlmUniqueID>(.*)<\/NlmUniqueID>/)[1]) - : [], - }) + } + } + submitData() { + if (this.state.enabled) { + const metadata = { + title: this.state.title, + } + if (this.state.journal.nlmuniqueid) { + metadata.journalMeta = this.state.journal } else { - this.setState({ - doSubmit: true, - journalNotExistsMessage: ( - <HTMLString - string={`This journal was not found. Do you want to submit '${ - this.state.journal - }' as your journal name?`} - /> - ), - }) + metadata.customMeta = { + unmatchedJournal: this.state.journal.title, + } } + metadata.specialInstructions = this.state.note + this.props.unmatchedInfo(metadata) } + } + onQueryChange(e) { + const query = e.target.value this.setState({ - enabled: true, - loading: false, + query: e ? query : '', + journals: [], }) + if (query.trim().length > 0) { + this.onSearch(query) + } } + async onSearch(query) { + const eSearch = `/eutils/esearch?db=nlmcatalog&term=${query}*[ta]%20AND%20ncbijournals[filter]&retstart=0` + const response = await fetch(eSearch, { + headers: new Headers({ + Authorization: `Bearer ${window.localStorage.getItem('token')}`, + }), + }) + const json = await response.json() + const ids = json.esearchresult.idlist + const hitcount = json.esearchresult.count - onQueryChange(event) { - if (event.target.name === 'Journal') { - this.setState({ - enabled: true, - doSubmit: false, - journal: event.target.value, + if (hitcount > 0) { + const eFetch = `/eutils/efetch?db=nlmcatalog&id=${ids.join()}` + const summary = await fetch(eFetch, { + headers: new Headers({ + Authorization: `Bearer ${window.localStorage.getItem('token')}`, + }), }) - } else if (event.target.name === 'Title') - this.setState({ + const xml = await summary.text() + const titles = xml.match(/<TitleMain>([\s\S]*?)<\/TitleMain>/g) + let nlmIds = xml.match(/<NlmUniqueID>(.*)<\/NlmUniqueID>/g) + nlmIds = nlmIds + ? nlmIds.map(x => x.match(/<NlmUniqueID>(.*)<\/NlmUniqueID>/)[1]) + : [] + const journals = titles + ? titles.map((x, i) => ({ + title: x.match(/<Title (.*)>(.*)<\/Title>/)[2], + nlmuniqueid: nlmIds[i], + })) + : [] + this.setState({ journals }) + } + } + onTitleChange(event) { + this.setState( + { titleMessage: '', - enabled: true, title: event.target.value, - }) + }, + () => { + this.enable() + }, + ) + } + onJournalChange(journal) { + this.setState( + { + journalMessage: '', + journal, + query: journal.title, + }, + () => { + this.enable() + }, + ) } - render() { return ( <div> - <Input> - <TextField - invalidTest={this.state.titleMessage} - label="Enter manuscript title" - name="Title" - onChange={this.onQueryChange} - placeholder="The title of your manuscript" - value={this.state.title} - /> - {this.state.titleMessage && ( - <ErrorText>{this.state.titleMessage}</ErrorText> - )} - </Input> - <SearchForm - buttonLabel={this.state.doSubmit ? 'Submit' : 'Search'} - disabled={!this.state.enabled} - label="Search Journals" - name="Journal" - onChange={this.onQueryChange} - onSubmit={this.onSearchForJournal} - placeholder="The journal name" - value={this.state.journal} + <H2>Enter citation information</H2> + <TextField + invalidTest={this.state.titleMessage} + label="Enter manuscript title" + name="Title" + onChange={this.onTitleChange} + placeholder="The title of your manuscript" + value={this.state.title} /> - {this.state.journalNotExistsMessage && ( - <p>{this.state.journalNotExistsMessage}</p> - )} - {this.state.journals.length > 0 && ( - <ResultList> - {this.state.journals.map((journal, index) => ( - <ResultListItem - key={this.state.ids[index]} - onClick={() => { - this.setState({ - journal, - // id: this.state.ids[index], - }) - if (this.state.title) { - this.props.getUnmatchedInfo({ - title: this.state.title, - nlmId: this.state.ids[index], - journal, - }) - } else { - this.setState({ titleMessage: 'Title is required.' }) - } - }} - > - <p> - <HTMLString string={journal} /> - </p> - </ResultListItem> - ))} - </ResultList> + {this.state.titleMessage && ( + <ErrorText>{this.state.titleMessage}</ErrorText> )} - {this.state.loading && ( - <Loading> - <LoadingIcon /> - </Loading> + <SearchSelect + invalidTest={this.state.journalMessage} + label="Search for Journal" + onInput={this.onQueryChange} + optionsOnChange={this.onJournalChange} + placeholder="Journal title abbreviation" + query={this.state.query} + selectedOptions={this.props.journal} + singleSelect + > + {this.state.journals.map(journal => ( + <SearchSelect.Option + data-option={journal} + key={journal.title + journal.id} + propKey={journal.title + journal.id} + > + <HTMLString string={journal.title} /> + </SearchSelect.Option> + ))} + {this.state.query.trim().length > 0 && ( + <SearchSelect.Option + data-option={{ title: this.state.query }} + key="submit" + propKey="submit" + > + <em> + <HTMLString + string={`Submit '${ + this.state.query + }' as your journal name`} + /> + </em> + </SearchSelect.Option> + )} + </SearchSelect> + {this.state.journalMessage && ( + <ErrorText>{this.state.journalMessage}</ErrorText> )} + <TextArea + label="Other journal information (optional)" + onChange={event => this.setState({ note: event.target.value })} + placeholder="List URL, DOI, Volume/Issue, etc." + rows={3} + value={this.state.note} + /> + <Buttons> + <Button + disabled={!this.state.enabled} + onClick={() => this.submitData()} + primary + > + Submit + </Button> + </Buttons> </div> ) } diff --git a/app/components/ui/atoms/Buttons.jsx b/app/components/ui/atoms/Buttons.jsx index 42ab63baa..81548721b 100644 --- a/app/components/ui/atoms/Buttons.jsx +++ b/app/components/ui/atoms/Buttons.jsx @@ -4,13 +4,14 @@ import { Button } from '@pubsweet/ui' const Buttons = styled.div` display: flex; + flex-direction: row-reverse; + align-items: center; justify-content: space-between; - flex-flow: row; margin: calc(${th('gridUnit')} * 3) 0 calc(${th('gridUnit')} * 4); div { - *:first-child { - margin: 0 20px; + *:first-child + * { + margin-left: 20px; } } ` diff --git a/app/components/ui/atoms/Page.jsx b/app/components/ui/atoms/Page.jsx index 136ae166d..35ec5f2e8 100644 --- a/app/components/ui/atoms/Page.jsx +++ b/app/components/ui/atoms/Page.jsx @@ -1,6 +1,17 @@ import styled from 'styled-components' import { th, override } from '@pubsweet/ui-toolkit' +const Cover = styled.div` + position: fixed; + right: 0; + left: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + background-color: ${th('colorTextReverse')}; + overflow: auto; +` const Page = styled.div` margin: 0 auto; max-width: 1000px; @@ -24,4 +35,7 @@ const Right = styled.div` const A = styled.a` ${override('ui.Link')}; ` -export { Page, Header, Center, Right, A } +const B = styled.strong` + font-weight: 600; +` +export { Cover, Page, Header, Center, Right, A, B } diff --git a/app/components/ui/atoms/TextArea.jsx b/app/components/ui/atoms/TextArea.jsx new file mode 100644 index 000000000..b85123e85 --- /dev/null +++ b/app/components/ui/atoms/TextArea.jsx @@ -0,0 +1,68 @@ +import React from 'react' +import styled from 'styled-components' +import { th, override } from '@pubsweet/ui-toolkit' + +const Root = styled.div` + display: flex; + flex-direction: column; + box-sizing: border-box; + max-width: 100%; + ${override('ui.TextArea')}; +` + +const Label = styled.label` + font-size: ${th('fontSizeBaseSmall')}; + line-height: ${th('lineHeightBaseSmall')}; + display: block; + ${override('ui.Label')}; + ${override('ui.TextArea.Label')}; +` + +const borderColor = ({ theme, validationStatus = 'default' }) => + ({ + error: theme.colorError, + success: theme.colorSuccess, + default: theme.colorBorder, + warning: theme.colorWarning, + }[validationStatus]) + +const Area = styled.textarea` + border: ${th('borderWidth')} ${th('borderStyle')} ${borderColor}; + border-radius: ${th('borderRadius')}; + font-family: inherit; + font-size: inherit; + padding: calc(${th('gridUnit')} * 1.5) ${th('gridUnit')}; + line-height: ${th('lineHeightBase')}; + box-sizing: border-box; + max-width: 100%; + &::placeholder { + color: ${th('colorTextPlaceholder')}; + } + + ${override('ui.TextArea.Area')}; +` + +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, rows = 5, ...props } = this.props + return ( + <Root> + {label && <Label htmlFor={this.inputId}>{label}</Label>} + <Area + id={this.inputId} + readOnly={readonly} + rows={rows} + value={value} + {...props} + /> + </Root> + ) + } +} + +export default TextArea diff --git a/app/components/ui/atoms/index.js b/app/components/ui/atoms/index.js index dbefc9b6a..8b1c261f9 100644 --- a/app/components/ui/atoms/index.js +++ b/app/components/ui/atoms/index.js @@ -2,6 +2,7 @@ export { LoadingIcon, Loading } from './LoadingIcon' export { CloseIcon, Close } from './CloseIcon' export { default as HTMLString } from './HTMLString' export { default as Select } from './Select' -export { Page, Header, Center, Right, A } from './Page' +export { default as TextArea } from './TextArea' +export { Cover, Page, Header, Center, Right, A, B } from './Page' export { SplitPage, StepPanel, InfoPanel } from './SplitPage' export { Buttons, IconButton } from './Buttons' diff --git a/app/components/ui/molecules/SearchSelect.jsx b/app/components/ui/molecules/SearchSelect.jsx index 3bf073110..9e8ceaa55 100644 --- a/app/components/ui/molecules/SearchSelect.jsx +++ b/app/components/ui/molecules/SearchSelect.jsx @@ -8,7 +8,9 @@ import { isEqual } from 'lodash' const Root = styled.div` display: flex; flex-direction: column; - margin-bottom: ${props => (props.inline ? '0' : props.theme.gridUnit)}; + margin-bottom: ${props => + props.inline ? '0' : `calc(${props.theme.gridUnit} * 3)`}; + ${override('ui.SearchSelect')}; ` const Label = styled.label` font-size: ${th('fontSizeBaseSmall')}; @@ -104,7 +106,7 @@ const SearchInput = styled.input` } ` const OptionList = styled.ul` - max-height: calc(${th('gridUnit')} * 42); + max-height: calc(${th('gridUnit')} * 21); background-color: ${th('colorBackground')}; overflow: auto; max-width: 100%; @@ -215,6 +217,8 @@ class SearchSelect extends React.Component { } } addOption(event, option) { + event.preventDefault() + event.stopPropagation() if (option) { if (!this.props.singleSelect) { const selectedOptions = [...this.state.selectedOptions] @@ -224,7 +228,10 @@ class SearchSelect extends React.Component { this.props.optionsOnChange(selectedOptions) } } else { - this.setState({ selectedOptions: option }) + this.setState({ + selectedOptions: option, + inFocus: false, + }) this.props.optionsOnChange(option) } } @@ -252,6 +259,7 @@ class SearchSelect extends React.Component { displaySelected, disabled, singleSelect = false, + placeholder = 'Find as you type...', ...props } = this.props let selectStyle = '6px 6px 6px 6px' @@ -301,13 +309,13 @@ class SearchSelect extends React.Component { onChange={onInput} onFocus={this.addFocus} onKeyDown={e => this.keyPressed(e)} - placeholder="Find as you type..." + placeholder={placeholder} type="text" value={query} /> </SelectBox> {children.some(child => { - if (children.length > 2) return true + if (children.length > 0) return true return child && child.length > 0 }) && ( <OptionList> diff --git a/app/redux/createsubmission.js b/app/redux/createsubmission.js index b3e382e21..2b1fbfae9 100644 --- a/app/redux/createsubmission.js +++ b/app/redux/createsubmission.js @@ -38,19 +38,22 @@ export const newManuscriptCitation = ( fragmentType, version, metadata, + notes, ...otherProps } = currentVersion const { fundingGroup, customMeta } = metadata + const { specialInstructions, ...other } = citationData + const newData = { ...other } if (fundingGroup) { - citationData.fundingGroup = fundingGroup + newData.fundingGroup = fundingGroup } if (customMeta) { const { releaseDelay } = customMeta if (releaseDelay) { - if (citationData.customMeta) { - citationData.customMeta.releaseDelay = releaseDelay + if (newData.customMeta) { + newData.customMeta.releaseDelay = releaseDelay } else { - citationData.customMeta = { + newData.customMeta = { releaseDelay, } } @@ -59,10 +62,15 @@ export const newManuscriptCitation = ( const newProps = { created: new Date(), // TODO: set on server fragmentType: 'version', - metadata: citationData, + metadata: newData, version: currentVersion.version + 1, ...otherProps, } + if (specialInstructions) { + newProps.notes = { + specialInstructions, + } + } dispatch(actions.createFragment(project, newProps)).then(({ fragment }) => { const currentPage = history.location.pathname.substr( history.location.pathname.lastIndexOf('/') + 1, diff --git a/app/routes.js b/app/routes.js index 8ce4860e0..54db9c1b1 100644 --- a/app/routes.js +++ b/app/routes.js @@ -33,7 +33,7 @@ const PrivateRoute = ({ component: Component, ...rest }) => ( {...rest} render={props => ( <AuthenticatedComponent> - <Component {...props} /> + <Component key={props.match.params.version} {...props} /> </AuthenticatedComponent> )} /> diff --git a/server/eutils/api.js b/server/eutils/api.js index c929392e5..0baee7dab 100644 --- a/server/eutils/api.js +++ b/server/eutils/api.js @@ -15,9 +15,9 @@ module.exports = app => { app.get('/eutils/esearch', authBearer, (req, res) => { res.set({ 'Content-Type': 'application/json' }) - const { term, db, retstart } = req.query + const { term, db, retstart, sort } = req.query const encodedTerm = encodeURIComponent(term) - const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=${db}&term=${encodedTerm}&retmode=json&retstart=${retstart}&retmax=25${ + const url = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=${db}&term=${encodedTerm}&sort=${sort}&retmode=json&retstart=${retstart}&retmax=25${ eutilsApiKey ? `&api_key=${eutilsApiKey}` : '' }` -- GitLab