diff --git a/packages/component-faraday-ui/package.json b/packages/component-faraday-ui/package.json index 4e2c1dafbe37ce15abce8f95cb7e0b73cb1b2d0c..4ace0cbd7c23bf4e57dad151a1c840c8c16871a3 100644 --- a/packages/component-faraday-ui/package.json +++ b/packages/component-faraday-ui/package.json @@ -6,9 +6,10 @@ "dependencies": { "@pubsweet/ui": "^8.3.0", "@pubsweet/ui-toolkit": "^1.2.0", + "grid-styled": "5.0.2", "prop-types": "^15.6.2", + "querystring": "^0.2.0", "react": "^16.4.2", - "styled-components": "^3.4.2", - "grid-styled": "5.0.2" + "styled-components": "^3.4.2" } } diff --git a/packages/component-faraday-ui/src/WizardAuthors.js b/packages/component-faraday-ui/src/WizardAuthors.js index c5cb13fe3581d2543a0ed58404c3eb6d98fbaf33..b9fd96d6eb6caa0bab0b6e2465459abe35a9a346 100644 --- a/packages/component-faraday-ui/src/WizardAuthors.js +++ b/packages/component-faraday-ui/src/WizardAuthors.js @@ -8,6 +8,7 @@ import { Row, Item, Label, + Text, DragHandle, AuthorCard, ActionLink, @@ -15,8 +16,9 @@ import { } from './' const WizardAuthors = ({ - authors = [], + error, moveAuthor, + authors = [], addNewAuthor, setAuthorEdit, saveNewAuthor, @@ -56,6 +58,11 @@ const WizardAuthors = ({ /> </SortableContainer> )} + {error && ( + <Row mb={1}> + <Text error>{error}</Text> + </Row> + )} </Fragment> ) @@ -68,7 +75,7 @@ export default compose( }, }), withHandlers({ - addNewAuthor: ({ authors, setAuthors }) => () => { + addNewAuthor: ({ authors = [], setAuthors }) => () => { if (authors.some(a => a.id === 'newAuthor')) { return } diff --git a/packages/component-faraday-ui/src/WizardFiles.js b/packages/component-faraday-ui/src/WizardFiles.js new file mode 100644 index 0000000000000000000000000000000000000000..9a1557d59ee637f7245a5f1994c1e438ae93d631 --- /dev/null +++ b/packages/component-faraday-ui/src/WizardFiles.js @@ -0,0 +1,117 @@ +import React, { Fragment } from 'react' +import { get } from 'lodash' +import { compose, withState, withHandlers } from 'recompose' + +import { FileSection, SortableList } from './' +import { withFileDownload, withFilePreview } from './helpers' + +const WizardFiles = ({ + files, + addFile, + moveFile, + deleteFile, + changeList, + previewFile, + downloadFile, +}) => ( + <Fragment> + <FileSection + allowedFileExtensions={['pdf', 'doc', 'docx']} + changeList={changeList} + files={get(files, 'manuscripts', [])} + isFirst + listId="manuscripts" + moveItem={moveFile('manuscripts')} + onDelete={deleteFile('manuscripts')} + onDownload={downloadFile} + onFileDrop={addFile('manuscripts')} + onFilePick={addFile('manuscripts')} + onPreview={previewFile} + required + title="Main Manuscript" + /> + <FileSection + allowedFileExtensions={['pdf', 'doc', 'docx']} + changeList={changeList} + files={get(files, 'coverLetter', [])} + listId="coverLetter" + maxFiles={1} + moveItem={moveFile('coverLetter')} + onDelete={deleteFile('coverLetter')} + onDownload={downloadFile} + onFileDrop={addFile('coverLetter')} + onFilePick={addFile('coverLetter')} + onPreview={previewFile} + required + title="Cover Letter" + /> + <FileSection + changeList={changeList} + files={get(files, 'supplementary', [])} + isLast + listId="supplementary" + moveItem={moveFile('supplementary')} + onDelete={deleteFile('supplementary')} + onDownload={downloadFile} + onFileDrop={addFile('supplementary')} + onFilePick={addFile('supplementary')} + onPreview={previewFile} + title="Supplimental Files" + /> + </Fragment> +) + +export default compose( + withFilePreview, + withFileDownload, + withState('files', 'setFiles', ({ files }) => files), + withHandlers({ + setFormFiles: ({ changeForm, setFiles }) => files => { + setFiles(files) + changeForm('submission', 'files', files) + }, + }), + withHandlers({ + addFile: ({ uploadFile, files, version, setFormFiles }) => type => file => { + uploadFile(file, type, version).then(f => { + const newFiles = { + ...files, + [type]: [...files[type], f], + } + setFormFiles(newFiles) + }) + }, + downloadFile: ({ downloadFile, token }) => file => { + downloadFile({ fileId: file.id, token, fileName: file.name }) + }, + deleteFile: ({ deleteFile, files, setFormFiles }) => type => file => { + deleteFile(file.id, type).then(() => { + const newFiles = { + ...files, + [type]: files[type].filter(f => f.id !== file.id), + } + setFormFiles(newFiles) + }) + }, + moveFile: ({ files, setFormFiles }) => type => (dragIndex, hoverIndex) => { + const newFiles = { + ...files, + [type]: SortableList.moveItem(files[type], dragIndex, hoverIndex), + } + setFormFiles(newFiles) + }, + previewFile: ({ previewFile }) => file => { + previewFile(file) + }, + changeList: ({ files, setFormFiles }) => (from, to, fileId) => { + const swappedFile = files[from].find(f => f.id === fileId) + + const newFiles = { + ...files, + [to]: [...files[to], swappedFile], + [from]: files[from].filter(f => f.id !== fileId), + } + setFormFiles(newFiles) + }, + }), +)(WizardFiles) diff --git a/packages/component-faraday-ui/src/WizardFiles.md b/packages/component-faraday-ui/src/WizardFiles.md new file mode 100644 index 0000000000000000000000000000000000000000..8495cb39d8d10b9321315f5f4ef2b7fb80f2c52c --- /dev/null +++ b/packages/component-faraday-ui/src/WizardFiles.md @@ -0,0 +1,33 @@ +A three section file component used in the submission flow. + +```js +const files = { + manuscripts: [ + { + id: 'file1', + name: 'myfile.docx', + size: 51312, + }, + { + id: 'file2', + name: 'another_pdf.pdf', + size: 133127, + }, + { + id: 'file3', + name: 'another_pdf1231.pdf', + size: 1337, + }, + ], + coverLetter: [ + { + id: 'cover1', + name: 'myfile.pdf', + size: 312, + }, + ], + supplementary: [], +}; + +<WizardFiles files={files} /> +``` diff --git a/packages/component-faraday-ui/src/helpers/index.js b/packages/component-faraday-ui/src/helpers/index.js index f214913aec8094c8764a375cad8e0dcbe3142bf2..fdb100ed3c6ae307c079422034d3a5d61a359863 100644 --- a/packages/component-faraday-ui/src/helpers/index.js +++ b/packages/component-faraday-ui/src/helpers/index.js @@ -1,5 +1,7 @@ import * as validators from './formValidators' +export { default as withFilePreview } from './withFilePreview' +export { default as withFileDownload } from './withFileDownload' export { default as withNativeFileDrop } from './withNativeFileDrop' export { default as withFileSectionDrop } from './withFileSectionDrop' diff --git a/packages/component-faraday-ui/src/helpers/withFileDownload.js b/packages/component-faraday-ui/src/helpers/withFileDownload.js new file mode 100644 index 0000000000000000000000000000000000000000..132ad70c96698851eef1ab8f1836d926a9835d11 --- /dev/null +++ b/packages/component-faraday-ui/src/helpers/withFileDownload.js @@ -0,0 +1,50 @@ +import qs from 'querystring' +import { withHandlers } from 'recompose' + +const createAnchorElement = (file, filename) => { + const url = URL.createObjectURL(file) + const a = document.createElement('a') + + a.href = url + a.download = filename + document.body.appendChild(a) + + return { + a, + url, + } +} + +const removeAnchorElement = (a, url) => { + document.body.removeChild(a) + URL.revokeObjectURL(url) +} + +export default withHandlers({ + downloadFile: () => ({ fileId, token, fileName = 'file' }) => { + if (!token) return + + const fileURL = `${ + window.location.origin + }/api/files/${fileId}?${qs.stringify({ + download: true, + })}` + + const xhr = new XMLHttpRequest() + xhr.onreadystatechange = function onXhrStateChange() { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + const f = new File([this.response], fileName) + + const { a, url } = createAnchorElement(f, fileName) + a.click() + removeAnchorElement(a, url) + } + } + } + xhr.open('GET', fileURL) + xhr.responseType = 'blob' + xhr.setRequestHeader('Authorization', `Bearer ${token}`) + xhr.send() + }, +}) diff --git a/packages/component-faraday-ui/src/helpers/withFilePreview.js b/packages/component-faraday-ui/src/helpers/withFilePreview.js new file mode 100644 index 0000000000000000000000000000000000000000..895be5e0fbeaccdca00be74a3749a1df79084c38 --- /dev/null +++ b/packages/component-faraday-ui/src/helpers/withFilePreview.js @@ -0,0 +1,12 @@ +import { withHandlers } from 'recompose' + +export default withHandlers({ + previewFile: ({ getSignedUrl }) => file => { + if (!getSignedUrl) return + + const windowReference = window.open() + getSignedUrl(file.id).then(({ signedUrl }) => { + windowReference.location = signedUrl + }) + }, +}) diff --git a/packages/component-faraday-ui/src/helpers/withFileSectionDrop.js b/packages/component-faraday-ui/src/helpers/withFileSectionDrop.js index ce997caf006edd08b99ca897e83e73d5036bdab1..1fff85c036e3cca3ebd2111dbf60f4350b9f2bff 100644 --- a/packages/component-faraday-ui/src/helpers/withFileSectionDrop.js +++ b/packages/component-faraday-ui/src/helpers/withFileSectionDrop.js @@ -6,11 +6,11 @@ export default DropTarget( drop( { files, - maxFiles, setError, changeList, listId: toListId, allowedFileExtensions, + maxFiles = Number.MAX_SAFE_INTEGER, }, monitor, ) { diff --git a/packages/component-faraday-ui/src/index.js b/packages/component-faraday-ui/src/index.js index d07117a3554b72384abdbf49fe6e6143b2aff927..0808cec2411a0e20a4a40fad255d5f21813cdc3e 100644 --- a/packages/component-faraday-ui/src/index.js +++ b/packages/component-faraday-ui/src/index.js @@ -1,4 +1,5 @@ export { default as ActionLink } from './ActionLink' +export { default as AuthorWithTooltip } from './AuthorWithTooltip' export { default as AppBar } from './AppBar' export { default as AppBarMenu } from './AppBarMenu' export { default as AuthorCard } from './AuthorCard' @@ -19,7 +20,7 @@ export { default as SortableList } from './SortableList' export { default as Tag } from './Tag' export { default as Text } from './Text' export { default as WizardAuthors } from './WizardAuthors' -export { default as AuthorWithTooltip } from './AuthorWithTooltip' +export { default as WizardFiles } from './WizardFiles' export * from './styledHelpers' diff --git a/packages/component-wizard/src/components/StepOne.js b/packages/component-wizard/src/components/StepOne.js index 59f383db4ce3638622fe296234965bdb5d1dea91..dccbbe93d33967933e138706017a2e14556e092b 100644 --- a/packages/component-wizard/src/components/StepOne.js +++ b/packages/component-wizard/src/components/StepOne.js @@ -66,7 +66,7 @@ const StepOne = () => ( account. </Text> </Row> - <Row alignItems="center" justify="center" mb={6} mt={1}> + <Row alignItems="center" justify="center" mb={4} mt={1}> <ValidatedField component={CustomCheckbox} name="declarations.agree" /> </Row> </Fragment> @@ -98,7 +98,7 @@ const RootCheckbox = styled.div.attrs({ } ` -const CustomH2 = H2.extend` +const CustomH2 = styled(H2)` margin: 0; ` // #endregion diff --git a/packages/component-wizard/src/components/StepThree.js b/packages/component-wizard/src/components/StepThree.js index 95847301c8ce43a7d60b56d65a59283c96cb6df1..344f00681324767612a4e4911f5c65a5cb48c5cd 100644 --- a/packages/component-wizard/src/components/StepThree.js +++ b/packages/component-wizard/src/components/StepThree.js @@ -1,32 +1,21 @@ -/* eslint-disable */ import React, { Fragment } from 'react' -import { H2, Icon } from '@pubsweet/ui' +import { get } from 'lodash' import { Field } from 'redux-form' -import { Files } from 'pubsweet-components-faraday/src/components' -import { - Row, - Text, - Item, - Label, - FileSection, -} from 'pubsweet-component-faraday-ui' +import { H2, Icon } from '@pubsweet/ui' +import { Row, Text, WizardFiles } from 'pubsweet-component-faraday-ui' import { Empty } from './' -const files = [ - { - id: 'file1', - name: 'myfile.docx', - size: 51312, - }, - { - id: 'file2', - name: 'another_pdf.pdf', - size: 133127, - }, -] - -const StepThree = ({ version, project, filesError }) => ( +const StepThree = ({ + token, + version, + project, + changeForm, + deleteFile, + uploadFile, + filesError, + getSignedUrl, +}) => ( <Fragment> <CustomH2>3. Manuscript Files Upload</CustomH2> <Row mb={2}> @@ -42,74 +31,23 @@ const StepThree = ({ version, project, filesError }) => ( icon to reorder or move files to a different type. </Text> </Row> - {/* <Row> - <Item vertical> - <Label>Files</Label> - <Field component={Empty} name="files" /> - <Files parentForm="submission" project={project} version={version} /> - {filesError && ( - <Text align="left" error> - {filesError} - </Text> - )} - </Item> - </Row> */} - <FileSection - allowedFileExtensions={['pdf', 'doc', 'docx']} - changeList={(from, to, fileId) => - console.log('change from to', from, to, fileId) - } - files={files} - isFirst - listId="mainManuscript" - moveItem={(dragIndex, hoverIndex) => - console.log('moving the item from', dragIndex, hoverIndex) - } - onDelete={f => console.log('delete', f)} - onDownload={f => console.log('download', f)} - onFileDrop={f => console.log('dropped a native file', f)} - onFilePick={f => console.log('picked a file', f)} - onPreview={f => console.log('preview', f)} - required - title="Main Manuscript" - /> - <FileSection - allowedFileExtensions={['pdf', 'doc', 'docx']} - changeList={(from, to, fileId) => - console.log('change from to', from, to, fileId) - } - files={files} - listId="coverLetter" - moveItem={(dragIndex, hoverIndex) => - console.log('moving the item from', dragIndex, hoverIndex) - } - onDelete={f => console.log('delete', f)} - onDownload={f => console.log('download', f)} - onFileDrop={f => console.log('dropped a native file', f)} - onFilePick={f => console.log('picked a file', f)} - onPreview={f => console.log('preview', f)} - required - title="Cover Letter" - /> - <FileSection - changeList={(from, to, fileId) => - console.log('change from to', from, to, fileId) - } - files={[]} - isLast - listId="supplimentalFiles" - moveItem={(dragIndex, hoverIndex) => - console.log('moving the item from', dragIndex, hoverIndex) - } - onDelete={f => console.log('delete', f)} - onDownload={f => console.log('download', f)} - onFileDrop={f => console.log('dropped a native file', f)} - onFilePick={f => console.log('picked a file', f)} - onPreview={f => console.log('preview', f)} - required - title="Supplimental Files" + <Field component={Empty} name="files" /> + <WizardFiles + changeForm={changeForm} + deleteFile={deleteFile} + files={get(version, 'files', {})} + getSignedUrl={getSignedUrl} + project={project} + token={token} + uploadFile={uploadFile} + version={version} /> + {filesError && ( + <Row mt={1}> + <Text error>{filesError}</Text> + </Row> + )} </Fragment> ) diff --git a/packages/component-wizard/src/components/StepTwo.js b/packages/component-wizard/src/components/StepTwo.js index 543b45eedc14329a4ee2c4d56705d3f7f411ff32..cf9e7586195ebc70b29cc3da8fa1e7d7b5dc8beb 100644 --- a/packages/component-wizard/src/components/StepTwo.js +++ b/packages/component-wizard/src/components/StepTwo.js @@ -37,7 +37,7 @@ const StepTwo = ({ </Row> <Row mb={1}> - <Item data-test="submission-title" flex={3} vertical withRightMargin> + <Item data-test-id="submission-title" flex={3} vertical withRightMargin> <Label required>MANUSCRIPT TITLE</Label> <ValidatedField component={TextField} @@ -45,7 +45,7 @@ const StepTwo = ({ validate={[required]} /> </Item> - <Item data-test="submission-type" vertical> + <Item data-test-id="submission-type" vertical> <Label required>MANUSCRIPT TYPE</Label> <ValidatedField component={input => <Menu options={manuscriptTypes} {...input} />} @@ -56,7 +56,7 @@ const StepTwo = ({ </Row> <Row mb={1}> - <Item data-test="submission-abstract" vertical> + <Item data-test-id="submission-abstract" vertical> <Label required>ABSTRACT</Label> <ValidatedField component={TextField} @@ -71,6 +71,7 @@ const StepTwo = ({ addAuthor={addAuthor} authors={get(version, 'authors', [])} changeForm={changeForm} + error={authorsError} project={project} version={version} /> @@ -99,7 +100,7 @@ const StepTwo = ({ </Row> {hasConflicts && ( - <Row alignItems="center" justify="flex-start" mb={4}> + <Row alignItems="center" justify="flex-start"> <Item data-test="submission-conflicts-text" vertical> <Label required>Conflict of interest details</Label> <ValidatedField diff --git a/packages/component-wizard/src/components/SubmissionWizard.js b/packages/component-wizard/src/components/SubmissionWizard.js index 3cd5f4f75b86e7cbf2b5abf1853e128dd528b86b..9bd4aec3436000a87255c7117203bb368ebfa525 100644 --- a/packages/component-wizard/src/components/SubmissionWizard.js +++ b/packages/component-wizard/src/components/SubmissionWizard.js @@ -18,6 +18,7 @@ import { withStateHandlers, } from 'recompose' import { Row } from 'pubsweet-component-faraday-ui' +import { getUserToken } from 'pubsweet-component-faraday-selectors/src' import { withModal, @@ -32,6 +33,11 @@ import { change as changeForm, } from 'redux-form' import { addAuthor } from 'pubsweet-components-faraday/src/redux/authors' +import { + uploadFile, + deleteFile, + getSignedUrl, +} from 'pubsweet-components-faraday/src/redux/files' import { wizardSteps } from './' import { @@ -44,7 +50,7 @@ import { submitManuscript } from '../redux/conversion' import { onChange, onSubmit, setInitialValues, validate } from './utils' // #region wizard -const NewWizard = ({ +const Wizard = ({ step, history, prevStep, @@ -65,14 +71,20 @@ const NewWizard = ({ <StepRoot className="wizard-step"> {wizardSteps[step].component({ manuscriptTypes, ...rest })} - <Row> + <Row justify="center" mt={2}> {!isFirstStep && ( <Button data-test="submission-back" + mr={1} onClick={prevStep} >{`< BACK`}</Button> )} - <Button data-test="submission-next" onClick={handleSubmit} primary> + <Button + data-test="submission-next" + ml={isFirstStep ? 0 : 1} + onClick={handleSubmit} + primary + > {getButtonText()} </Button> </Row> @@ -101,6 +113,7 @@ export default compose( withJournal, connect( (state, { match }) => ({ + token: getUserToken(state), formValues: getFormValues('submission')(state), submitFailed: hasSubmitFailed('submission')(state), formSyncErrors: getFormSyncErrors('submission')(state), @@ -110,6 +123,9 @@ export default compose( { addAuthor, changeForm, + uploadFile, + deleteFile, + getSignedUrl, autosaveRequest, autosaveSuccess, autosaveFailure, @@ -118,7 +134,7 @@ export default compose( }, ), withStateHandlers( - { step: 2 }, + { step: 0 }, { nextStep: ({ step }) => () => ({ step: Math.min(wizardSteps.length - 1, step + 1), @@ -130,10 +146,10 @@ export default compose( withProps(({ formValues, formSyncErrors, submitFailed, step, location }) => ({ isFirstStep: step === 0, isLastStep: step === wizardSteps.length - 1, + isEditMode: get(location, 'state.editMode', false), filesError: submitFailed && get(formSyncErrors, 'files', ''), authorsError: submitFailed && get(formSyncErrors, 'authors', ''), hasConflicts: get(formValues, 'conflicts.hasConflicts', 'no') === 'yes', - isEditMode: get(location, 'state.editMode', false), })), withHandlers({ getButtonText: ({ isLastStep, isEditMode }) => () => { @@ -154,7 +170,7 @@ export default compose( }), DragDropContext(HTML5Backend), toClass, -)(NewWizard) +)(Wizard) // #endregion // #region styled-components @@ -172,6 +188,7 @@ const StepRoot = styled.div` box-shadow: ${th('boxShadow')}; display: flex; flex-direction: column; + margin-top: ${th('gridUnit')}; padding: calc(${th('gridUnit')} * 5) calc(${th('gridUnit')} * 4); position: relative; width: 100%; diff --git a/packages/hindawi-theme/src/elements/Button.js b/packages/hindawi-theme/src/elements/Button.js index 520a979d672fd036346f8caadbb050854ad4c0fe..34720cd8f3d58e012918c38dd57736cb8e676030 100644 --- a/packages/hindawi-theme/src/elements/Button.js +++ b/packages/hindawi-theme/src/elements/Button.js @@ -1,5 +1,6 @@ import { css } from 'styled-components' import { lighten, th } from '@pubsweet/ui-toolkit' +import { marginHelper } from 'pubsweet-component-faraday-ui' const primary = css` background-color: ${th('button.primary')}; @@ -82,4 +83,5 @@ export default css` ${props => (props.primary ? primary : secondary)}; ${buttonSize}; + ${marginHelper}; ` diff --git a/packages/xpub-faraday/app/FaradayApp.js b/packages/xpub-faraday/app/FaradayApp.js index 1ef508a8c807911d838522e92a2e169e9b746820..51b13a81a9773831c2bec76b303c561875169125 100644 --- a/packages/xpub-faraday/app/FaradayApp.js +++ b/packages/xpub-faraday/app/FaradayApp.js @@ -6,11 +6,17 @@ import { actions } from 'pubsweet-client' import { withJournal } from 'xpub-journal' import { withRouter } from 'react-router-dom' import { compose, withHandlers } from 'recompose' -import { AppBar, Logo, AppBarMenu } from 'pubsweet-component-faraday-ui' +import { + Logo, + AppBar, + AppBarMenu, + AutosaveIndicator, +} from 'pubsweet-component-faraday-ui' -const App = ({ journal, goTo, children, logout, currentUser }) => ( +const App = ({ autosave, journal, goTo, children, logout, currentUser }) => ( <Root className="faraday-root"> <AppBar + autosave={() => <AutosaveIndicator autosave={autosave} delay={4000} />} journal={journal} logo={() => ( <Logo @@ -30,6 +36,7 @@ const App = ({ journal, goTo, children, logout, currentUser }) => ( export default compose( connect( state => ({ + autosave: state.autosave, currentUser: state.currentUser, }), { logout: actions.logoutUser },