From 8ca31db153e7c94d3884731faf09e7f1a26f9b6d Mon Sep 17 00:00:00 2001 From: Jure Triglav <juretriglav@gmail.com> Date: Fri, 11 Sep 2020 11:56:54 +0200 Subject: [PATCH] feat: focus on errors in submission after submit --- .../src/components/FormTemplate.js | 41 ++++------- .../src/components/SubmitPage.js | 20 ++++-- .../src/components/ValidatedField.js | 69 +++++++++++++++++++ app/theme/elements/TextField.js | 21 ++++-- app/theme/index.js | 2 +- cypress/integration/001-submission_spec.js | 16 +---- .../submission_with_errors_spec.js | 51 ++++++++++++++ 7 files changed, 166 insertions(+), 54 deletions(-) create mode 100644 app/components/component-submit/src/components/ValidatedField.js create mode 100644 cypress/integration/submission_with_errors_spec.js diff --git a/app/components/component-submit/src/components/FormTemplate.js b/app/components/component-submit/src/components/FormTemplate.js index 1610b24e4e..b5620371a7 100644 --- a/app/components/component-submit/src/components/FormTemplate.js +++ b/app/components/component-submit/src/components/FormTemplate.js @@ -8,7 +8,6 @@ import { CheckboxGroup, Button, Attachment, - ValidatedFieldFormik, } from '@pubsweet/ui' import * as validators from 'xpub-validators' import { AbstractEditor } from 'xpub-edit' @@ -16,30 +15,9 @@ import { Section as Container, Select, FilesUpload } from '../../../shared' import { Heading1, Section, Legend, SubNote } from '../style' import AuthorsInput from './AuthorsInput' import LinksInput from './LinksInput' +import ValidatedFieldFormik from './ValidatedField' import Confirm from './Confirm' -// TODO: https://github.com/formium/formik/issues/146#issuecomment-474775723 -// const useFocusOnError = ({ fieldRef, name }) => { -// const formik = useFormikContext() -// const prevSubmitCountRef = React.useRef(formik.submitCount) -// const firstErrorKey = Object.keys(formik.errors)[0] -// React.useEffect(() => { -// if (prevSubmitCountRef.current !== formik.submitCount && !formik.isValid) { -// if (fieldRef.current && firstErrorKey === name) fieldRef.current.focus() -// } -// prevSubmitCountRef.current = formik.submitCount -// }, [formik.submitCount, formik.isValid, firstErrorKey]) -// } - -// const Wrapper = styled.div` -// font-family: ${th('fontInterface')}; -// line-height: 1.3; -// margin: auto; -// max-width: 60em; - -// overflow: ${({ confirming }) => confirming && 'hidden'}; -// ` - const Intro = styled.div` font-style: italic; line-height: 1.4; @@ -57,9 +35,6 @@ const ModalWrapper = styled.div` top: 0; ` -// Due to migration to new Data Model -// Attachement component needs different data structure to work -// needs to change the pubsweet ui Attachement to support the new Data Model const filesToAttachment = file => ({ name: file.filename, url: file.url, @@ -347,7 +322,7 @@ export default ({ ) : null} {values.status !== 'submitted' && form.haspopup === 'false' && ( - <Button onClick={handleSubmit} primary type="submit"> + <Button onClick={() => handleSubmit()} primary type="submit"> Submit your research object </Button> )} @@ -355,7 +330,17 @@ export default ({ {values.status !== 'submitted' && form.haspopup === 'true' && ( <div> <Button - onClick={() => validateForm() && toggleConfirming()} + onClick={async () => { + const hasErrors = Object.keys(await validateForm()).length !== 0 + + // If there are errors, do a fake submit + // to focus on the error + if (hasErrors) { + handleSubmit() + } else { + toggleConfirming() + } + }} primary type="button" > diff --git a/app/components/component-submit/src/components/SubmitPage.js b/app/components/component-submit/src/components/SubmitPage.js index 9012c140e8..da4fcb32c6 100644 --- a/app/components/component-submit/src/components/SubmitPage.js +++ b/app/components/component-submit/src/components/SubmitPage.js @@ -139,8 +139,18 @@ const SubmitPage = ({ match, history, ...props }) => { if (error) return JSON.stringify(error) const manuscript = data?.manuscript - const getFile = data?.getFile - + const form = data?.getFile + + // Set the initial values based on the form + let initialValues = {} + const fieldNames = form.children.map(field => field.name) + fieldNames.forEach(fieldName => set(initialValues, fieldName, null)) + initialValues = Object.assign({}, manuscript, { + submission: Object.assign( + initialValues.submission, + JSON.parse(manuscript.submission), + ), + }) const updateManuscript = input => update({ variables: { @@ -176,9 +186,7 @@ const SubmitPage = ({ match, history, ...props }) => { <Formik displayName="submit" handleChange={handleChange} - initialValues={Object.assign({}, manuscript, { - submission: JSON.parse(manuscript.submission), - })} + initialValues={initialValues} onSubmit={async (values, { validateForm, setSubmitting, ...other }) => { // TODO: Change this to a more Formik idiomatic form const isValid = Object.keys(await validateForm()).length === 0 @@ -188,7 +196,7 @@ const SubmitPage = ({ match, history, ...props }) => { {props => ( <Submit confirming={confirming} - forms={cloneDeep(getFile)} + forms={cloneDeep(form)} manuscript={manuscript} onChange={handleChange} toggleConfirming={toggleConfirming} diff --git a/app/components/component-submit/src/components/ValidatedField.js b/app/components/component-submit/src/components/ValidatedField.js new file mode 100644 index 0000000000..36fad0c651 --- /dev/null +++ b/app/components/component-submit/src/components/ValidatedField.js @@ -0,0 +1,69 @@ +import React from 'react' +import { FastField, ErrorMessage, useFormikContext } from 'formik' +import { get } from 'lodash' +import styled from 'styled-components' +import { th } from '@pubsweet/ui-toolkit' + +const MessageWrapper = styled.div` + font-family: ${th('fontInterface')}; + display: flex; + color: ${th('colorError')}; + &:not(:last-child) { + margin-bottom: ${th('gridUnit')}; + } + font-size: ${th('fontSizeBaseSmall')}; + line-height: ${th('lineHeightBaseSmall')}; +` + +// const ValidatedFieldComponent = ({ component: Component }) => +const useFocusOnError = ({ fieldRef, name }) => { + const formik = useFormikContext() + const prevSubmitCountRef = React.useRef(formik.submitCount) + let firstErrorKey = Object.keys(formik.errors)[0] + // If the error is in a nested object, we need to look deeper + // e.g. errors = { submission: { name: 'error' } } + const nestedError = formik.errors[firstErrorKey] + if (nestedError !== null && typeof nestedError === 'object') { + firstErrorKey = `${firstErrorKey}.${Object.keys(nestedError)[0]}` // e.g. submission.name + } + React.useEffect(() => { + if (prevSubmitCountRef.current !== formik.submitCount && !formik.isValid) { + if (fieldRef.current && firstErrorKey === name) fieldRef.current.focus() + } + prevSubmitCountRef.current = formik.submitCount + }, [formik.submitCount, formik.isValid, firstErrorKey]) +} + +const ValidatedField = ({ component: Component, ...props }) => { + // TODO: https://github.com/formium/formik/issues/146#issuecomment-474775723 + const fieldRef = React.useRef() + const { name } = props + useFocusOnError({ fieldRef, name }) + + return ( + <FastField {...props}> + {({ form: { errors, touched }, field, meta }) => { + let validationStatus + if (get(touched, name)) validationStatus = 'success' + if (get(touched, name) && get(errors, name)) validationStatus = 'error' + return ( + <div> + <Component + {...field} + {...props} + innerRefProp={fieldRef} + validationStatus={validationStatus} + /> + + {/* live region DOM node must be initially present for changes to be announced */} + <MessageWrapper role="alert"> + <ErrorMessage name={name} /> + </MessageWrapper> + </div> + ) + }} + </FastField> + ) +} + +export default ValidatedField diff --git a/app/theme/elements/TextField.js b/app/theme/elements/TextField.js index acf5abc04e..da8f4ac0ca 100644 --- a/app/theme/elements/TextField.js +++ b/app/theme/elements/TextField.js @@ -2,14 +2,11 @@ import { css } from 'styled-components' import { th, grid } from '@pubsweet/ui-toolkit' export default { - // TODO - // -- input padding: breaking the grid? - // -- small placeholder text? maybe by default? Input: css` border-color: ${props => { switch (props.validationStatus) { case 'success': - return props.theme.colorSuccess + return props.theme.colorBorder case 'warning': return props.theme.colorWarning case 'error': @@ -21,7 +18,7 @@ export default { color: ${props => { switch (props.validationStatus) { case 'success': - return props.theme.colorSuccess + return props.theme.colorText case 'warning': return props.theme.colorWarning case 'error': @@ -34,7 +31,19 @@ export default { transition: ${th('transitionDuration')} ${th('transitionTimingFunction')}; &:focus { - border-color: ${th('colorPrimary')}; + box-shadow: ${th('boxShadow')}; + border-color: ${props => { + switch (props.validationStatus) { + case 'success': + return props.theme.colorSuccess + case 'warning': + return props.theme.colorWarning + case 'error': + return props.theme.colorError + default: + return props.theme.colorPrimary + } + }}; color: inherit; } diff --git a/app/theme/index.js b/app/theme/index.js index 9ecac132a2..1818f2e6cf 100644 --- a/app/theme/index.js +++ b/app/theme/index.js @@ -73,7 +73,7 @@ const cokoTheme = { // $borderColor: var($colorFurniture); /* Shadow (for tooltip) */ - boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', + boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 4px 0 rgba(0, 0, 0, 0.06)', // boxShadow: '4px 4px 16px #cdcdcd', /* Transition */ transitionDuration: '0.2s', // TODO -- julien: not 0.05s diff --git a/cypress/integration/001-submission_spec.js b/cypress/integration/001-submission_spec.js index b43d1ea9e3..992c05fb40 100644 --- a/cypress/integration/001-submission_spec.js +++ b/cypress/integration/001-submission_spec.js @@ -1,18 +1,8 @@ -// const URLsubmission = { -// title: -// } -const login = name => { - cy.task('createToken', name).then(token => { - cy.setToken(token) - cy.visit('/journal/dashboard') - }) -} - describe('URL submission test', () => { it('can submit a URL and some metadata', () => { cy.task('restore', 'initialState') - login('Emily Clay') + cy.login('Emily Clay') cy.get('button') .contains('New submission') @@ -143,7 +133,7 @@ describe('URL submission test', () => { cy.task('restore', 'submission_complete') // Admin logs in to assign senior editor - login('Sinead Sullivan') + cy.login('Sinead Sullivan') cy.get('nav') .contains('Manuscripts') .click() @@ -157,7 +147,7 @@ describe('URL submission test', () => { .contains('Joanne Pilger') .click() - login('Joanne Pilger') + cy.login('Joanne Pilger') cy.contains('My URL submission') cy.contains('Control Panel').click() diff --git a/cypress/integration/submission_with_errors_spec.js b/cypress/integration/submission_with_errors_spec.js new file mode 100644 index 0000000000..999a73f2d2 --- /dev/null +++ b/cypress/integration/submission_with_errors_spec.js @@ -0,0 +1,51 @@ +describe('Submission with errors test', () => { + it('can submit a URL and some metadata', () => { + cy.task('restore', 'initialState') + + cy.login('Emily Clay') + + cy.get('button') + .contains('New submission') + .click() + cy.get('button') + .contains('Submit a URL instead') + .click() + + cy.get('body').contains('Submission created') + + cy.get('button') + .contains('Submit your research object') + .click() + + cy.get('body').contains('Enter at least 4 characters') + + cy.get('[data-testid="submission.name"]') + .click() + .type('Emily Clay') + + cy.get('button') + .contains('Submit your research object') + .click() + + cy.get('body').contains('Enter at least 2 characters') + + cy.get('[data-testid="submission.keywords"]') + .click() + .type('some, keywords') + + // Change the title so that we can look for it + cy.get('input[data-testid="meta.title"]') + .click() + .clear() + .type('My Fixed URL Submission') + + cy.get('button') + .contains('Submit your research object') + .click() + cy.get('button') + .contains('Submit your manuscript') + .click() + + cy.get('body').contains('My Fixed URL Submission') + }) +}) -- GitLab