diff --git a/labware-library/src/labware-creator/components/Section.tsx b/labware-library/src/labware-creator/components/Section.tsx deleted file mode 100644 index 707926b0c2e..00000000000 --- a/labware-library/src/labware-creator/components/Section.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react' -import compact from 'lodash/compact' -import uniq from 'lodash/uniq' -import { connect } from 'formik' -import { AlertItem } from '@opentrons/components' -import { getIsHidden } from '../formSelectors' -import { LinkOut } from './LinkOut' -import styles from './Section.css' -import { IRREGULAR_LABWARE_ERROR, LINK_CUSTOM_LABWARE_FORM } from '../fields' -import type { LabwareFields } from '../fields' - -// TODO: Make this DRY, don't require fields (in children) and also fieldList. -interface Props { - label: string - formik?: any // TODO IMMEDIATELY type this?? - additionalAlerts?: React.ReactNode - fieldList?: Array - children?: React.ReactNode - headingClassName?: string -} - -export const Section = connect((props: Props) => { - const fieldList = props.fieldList || [] - if (props.fieldList != null && fieldList.length > 0) { - const numFieldsHidden = props.fieldList - .map(field => getIsHidden(field, props.formik.values)) - .filter(Boolean).length - - if (numFieldsHidden === fieldList.length) { - // all fields are hidden, don't render this Section - return null - } - } - - // show Formik errors (from Yup) as WARNINGs for all dirty fields within this Section - const dirtyFieldNames = fieldList.filter( - name => props.formik?.touched?.[name] - ) - const allErrors: string[] = uniq( - compact(dirtyFieldNames.map(name => props.formik.errors[name])) - ) - - const allErrorAlerts = allErrors.map(error => { - if (error === IRREGULAR_LABWARE_ERROR) { - return ( - - Your labware is not compatible with the Labware Creator. Please - fill out{' '} - this form to - request a custom labware definition. - - } - /> - ) - } - return - }) - - return ( -
-

- {props.label} -

-
- {allErrorAlerts} - {props.additionalAlerts} -
- {props.children} -
- ) -}) diff --git a/labware-library/src/labware-creator/components/__tests__/sections/Description.test.tsx b/labware-library/src/labware-creator/components/__tests__/sections/Description.test.tsx new file mode 100644 index 00000000000..a51d5a6b49e --- /dev/null +++ b/labware-library/src/labware-creator/components/__tests__/sections/Description.test.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { when } from 'jest-when' +import { FormikConfig } from 'formik' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import { getDefaultFormState, LabwareFields } from '../../../fields' +import { isEveryFieldHidden } from '../../../utils' +import { Description } from '../../sections/Description' +import { wrapInFormik } from '../../utils/wrapInFormik' + +jest.mock('../../../utils') + +const isEveryFieldHiddenMock = isEveryFieldHidden as jest.MockedFunction< + typeof isEveryFieldHidden +> + +let formikConfig: FormikConfig + +describe('Description', () => { + beforeEach(() => { + formikConfig = { + initialValues: getDefaultFormState(), + onSubmit: jest.fn(), + } + + when(isEveryFieldHiddenMock) + .calledWith(['brand', 'brandId'], formikConfig.initialValues) + .mockReturnValue(false) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('should render fields when fields are visible', () => { + render(wrapInFormik(, formikConfig)) + expect(screen.getByRole('heading')).toHaveTextContent(/description/i) + + // TODO IMMEDIATELY: changes from 7715 ??? + screen.getByRole('textbox', { name: /^brand$/i }) + screen.getByRole('textbox', { name: /manufacturer\/catalog #/i }) + }) + + it('should render alert when error is present', () => { + const FAKE_ERROR = 'ahh' + formikConfig.initialErrors = { brand: FAKE_ERROR } + formikConfig.initialTouched = { brand: true } + render(wrapInFormik(, formikConfig)) + + // TODO(IL, 2021-05-26): AlertItem should have role="alert", then we can `getByRole('alert', {name: FAKE_ERROR})` + screen.getByText(FAKE_ERROR) + }) + + it('should not render when all of the fields are hidden', () => { + when(isEveryFieldHiddenMock) + .calledWith(['brand', 'brandId'], formikConfig.initialValues) + .mockReturnValue(true) + + const { container } = render(wrapInFormik(, formikConfig)) + expect(container.firstChild).toBe(null) + }) +}) diff --git a/labware-library/src/labware-creator/components/__tests__/sections/Export.test.tsx b/labware-library/src/labware-creator/components/__tests__/sections/Export.test.tsx new file mode 100644 index 00000000000..f2a783685c0 --- /dev/null +++ b/labware-library/src/labware-creator/components/__tests__/sections/Export.test.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { when } from 'jest-when' +import { FormikConfig } from 'formik' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import { getDefaultFormState, LabwareFields } from '../../../fields' +import { isEveryFieldHidden } from '../../../utils' +import { Export } from '../../sections/Export' +import { wrapInFormik } from '../../utils/wrapInFormik' + +jest.mock('../../../utils') + +const isEveryFieldHiddenMock = isEveryFieldHidden as jest.MockedFunction< + typeof isEveryFieldHidden +> + +let formikConfig: FormikConfig +let onExportClick: (e: any) => unknown + +describe('Export', () => { + beforeEach(() => { + formikConfig = { + initialValues: getDefaultFormState(), + onSubmit: jest.fn(), + } + + onExportClick = jest.fn() + + when(isEveryFieldHiddenMock) + .calledWith(['pipetteName'], formikConfig.initialValues) + .mockReturnValue(false) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('should render headings & fields when section is visible', () => { + render(wrapInFormik(, formikConfig)) + + const headings = screen.getAllByRole('heading') + expect(headings).toHaveLength(2) + expect(headings[0]).toHaveTextContent(/labware test protocol/i) + expect(headings[1]).toHaveTextContent(/please test your definition file/i) + + screen.getByText( + 'Your file will be exported with a protocol that will help you test and troubleshoot your labware definition on the robot. ' + + 'The protocol requires a Single Channel pipette on the right mount of your robot.' + ) + + screen.getByRole('textbox', { name: /test pipette/i }) + screen.getByRole('button', { name: /export/i }) + }) + + it('should render alert when error is present', () => { + const FAKE_ERROR = 'ahh' + formikConfig.initialErrors = { pipetteName: FAKE_ERROR } + formikConfig.initialTouched = { pipetteName: true } + render(wrapInFormik(, formikConfig)) + + // TODO(IL, 2021-05-26): AlertItem should have role="alert", then we can `getByRole('alert', {name: FAKE_ERROR})` + screen.getByText(FAKE_ERROR) + }) + + it('should not render when all of the fields are hidden', () => { + when(isEveryFieldHiddenMock) + .calledWith(['pipetteName'], formikConfig.initialValues) + .mockReturnValue(true) + + const { container } = render( + wrapInFormik(, formikConfig) + ) + expect(container.firstChild).toBe(null) + }) +}) diff --git a/labware-library/src/labware-creator/components/__tests__/sections/File.test.tsx b/labware-library/src/labware-creator/components/__tests__/sections/File.test.tsx new file mode 100644 index 00000000000..155c2d782c1 --- /dev/null +++ b/labware-library/src/labware-creator/components/__tests__/sections/File.test.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { when } from 'jest-when' +import { FormikConfig } from 'formik' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import { getDefaultFormState, LabwareFields } from '../../../fields' +import { isEveryFieldHidden } from '../../../utils' +import { File } from '../../sections/File' +import { wrapInFormik } from '../../utils/wrapInFormik' + +jest.mock('../../../utils') + +const isEveryFieldHiddenMock = isEveryFieldHidden as jest.MockedFunction< + typeof isEveryFieldHidden +> + +let formikConfig: FormikConfig + +describe('File', () => { + beforeEach(() => { + formikConfig = { + initialValues: getDefaultFormState(), + onSubmit: jest.fn(), + } + + when(isEveryFieldHiddenMock) + .calledWith(['loadName', 'displayName'], formikConfig.initialValues) + .mockReturnValue(false) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('should render fields when fields are visible', () => { + render(wrapInFormik(, formikConfig)) + expect(screen.getByRole('heading')).toHaveTextContent(/file/i) + + // TODO IMMEDIATELY: changes from 7715 + screen.getByRole('textbox', { name: /display name/i }) + screen.getByRole('textbox', { name: /api load name/i }) + }) + + it('should render alert when error is present', () => { + const FAKE_ERROR = 'ahh' + formikConfig.initialErrors = { displayName: FAKE_ERROR } + formikConfig.initialTouched = { displayName: true } + render(wrapInFormik(, formikConfig)) + + // TODO(IL, 2021-05-26): AlertItem should have role="alert", then we can `getByRole('alert', {name: FAKE_ERROR})` + screen.getByText(FAKE_ERROR) + }) + + it('should not render when all of the fields are hidden', () => { + when(isEveryFieldHiddenMock) + .calledWith(['loadName', 'displayName'], formikConfig.initialValues) + .mockReturnValue(true) + + const { container } = render(wrapInFormik(, formikConfig)) + expect(container.firstChild).toBe(null) + }) +}) diff --git a/labware-library/src/labware-creator/components/__tests__/sections/Preview.test.tsx b/labware-library/src/labware-creator/components/__tests__/sections/Preview.test.tsx new file mode 100644 index 00000000000..9276a1e19e5 --- /dev/null +++ b/labware-library/src/labware-creator/components/__tests__/sections/Preview.test.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { FormikConfig } from 'formik' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import { getDefaultFormState, LabwareFields } from '../../../fields' +import { Preview } from '../../sections/Preview' +import { wrapInFormik } from '../../utils/wrapInFormik' +import { FORM_LEVEL_ERRORS } from '../../../formLevelValidation' + +// NOTE(IL, 2021-05-18): eventual dependency on definitions.tsx which uses require.context +// would break this test (though it's not directly used) +jest.mock('../../../../definitions') + +let formikConfig: FormikConfig + +describe('Preview', () => { + beforeEach(() => { + formikConfig = { + initialValues: getDefaultFormState(), + onSubmit: jest.fn(), + } + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('should render the preview section', () => { + render(wrapInFormik(, formikConfig)) + expect(screen.getByRole('heading')).toHaveTextContent(/check your work/i) + screen.getByText( + 'Check that the size, spacing, and shape of your wells looks correct.' + ) + screen.getByText('Add missing info to see labware preview') + }) + + it('should render form-level alerts when form-level errors are present', () => { + const FAKE_ERROR = 'ahh' + // @ts-expect-error: fake form-level error + formikConfig.initialErrors = { [FORM_LEVEL_ERRORS]: { FAKE_ERROR } } + render(wrapInFormik(, formikConfig)) + + // TODO(IL, 2021-05-26): AlertItem should have role="alert", then we can `getByRole('alert', {name: FAKE_ERROR})` + screen.getByText(FAKE_ERROR) + }) +}) diff --git a/labware-library/src/labware-creator/components/sections/Description.tsx b/labware-library/src/labware-creator/components/sections/Description.tsx new file mode 100644 index 00000000000..f31c3cae1e1 --- /dev/null +++ b/labware-library/src/labware-creator/components/sections/Description.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { useFormikContext } from 'formik' +import { LabwareFields } from '../../fields' +import { isEveryFieldHidden } from '../../utils' +import { FormAlerts } from '../alerts/FormAlerts' +import { TextField } from '../TextField' +import { SectionBody } from './SectionBody' + +import styles from '../../styles.css' + +const Content = (): JSX.Element => ( +
+
+ +
+
+ +
+
+) + +export const Description = (): JSX.Element | null => { + const fieldList: Array = ['brand', 'brandId'] + const { values, errors, touched } = useFormikContext() + + if (isEveryFieldHidden(fieldList, values)) { + return null + } + + return ( + + + + + ) +} diff --git a/labware-library/src/labware-creator/components/sections/Export.tsx b/labware-library/src/labware-creator/components/sections/Export.tsx new file mode 100644 index 00000000000..0ff0775d5b8 --- /dev/null +++ b/labware-library/src/labware-creator/components/sections/Export.tsx @@ -0,0 +1,80 @@ +import cx from 'classnames' +import * as React from 'react' +import { useFormikContext } from 'formik' +import { PrimaryBtn } from '@opentrons/components' +import { reportEvent } from '../../../analytics' +import { LabwareFields } from '../../fields' +import { isEveryFieldHidden } from '../../utils' +import { pipetteNameOptions } from '../../labwareTestProtocol' +import { FormAlerts } from '../alerts/FormAlerts' +import { Dropdown } from '../Dropdown' +import { LinkOut } from '../LinkOut' +import { SectionBody } from './SectionBody' +import styles from '../../styles.css' + +const PDF_URL = + 'https://opentrons-publications.s3.us-east-2.amazonaws.com/labwareDefinition_testGuide.pdf' + +interface ExportProps { + onExportClick: (e: React.MouseEvent) => unknown +} + +export const Export = (props: ExportProps): JSX.Element | null => { + const fieldList: Array = ['pipetteName'] + const { values, errors, touched } = useFormikContext() + + if (isEveryFieldHidden(fieldList, values)) { + return null + } + + return ( + + + +
+
+

+ Your file will be exported with a protocol that will help you test + and troubleshoot your labware definition on the robot. The protocol + requires a Single Channel pipette on the right mount of your robot. +

+
+
+ +
+
+
+
+

+ Please test your definition file! +

+ +

+ Use the labware test protocol contained in the downloaded file to + check the accuracy of your definition. It’s important to create + definitions that are precise and do not rely on excessive + calibration prior to each run to achieve accuracy. +

+

Use the test guide to troubleshoot your definition.

+ + reportEvent({ + name: 'labwareCreatorClickTestLabware', + }) + } + href={PDF_URL} + className={styles.test_guide_button} + > + view test guide + +
+ + EXPORT FILE + +
+
+ ) +} diff --git a/labware-library/src/labware-creator/components/sections/File.tsx b/labware-library/src/labware-creator/components/sections/File.tsx new file mode 100644 index 00000000000..3fd9bc46f59 --- /dev/null +++ b/labware-library/src/labware-creator/components/sections/File.tsx @@ -0,0 +1,44 @@ +import * as React from 'react' +import { useFormikContext } from 'formik' +import { LabwareFields } from '../../fields' +import { maskLoadName } from '../../fieldMasks' +import { getDefaultDisplayName, getDefaultLoadName } from '../../formSelectors' +import { isEveryFieldHidden } from '../../utils' +import { FormAlerts } from '../alerts/FormAlerts' +import { TextField } from '../TextField' +import { SectionBody } from './SectionBody' + +import styles from '../../styles.css' + +const Content = (props: { values: LabwareFields }): JSX.Element => ( +
+
+ + +
+
+) + +export const File = (): JSX.Element | null => { + const fieldList: Array = ['loadName', 'displayName'] + const { values, errors, touched } = useFormikContext() + + if (isEveryFieldHidden(fieldList, values)) { + return null + } + + return ( + + + + + ) +} diff --git a/labware-library/src/labware-creator/components/sections/Preview.tsx b/labware-library/src/labware-creator/components/sections/Preview.tsx new file mode 100644 index 00000000000..f420a69d38d --- /dev/null +++ b/labware-library/src/labware-creator/components/sections/Preview.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' +import { useFormikContext } from 'formik' +import { LabwareFields } from '../../fields' +import { ConditionalLabwareRender } from '../ConditionalLabwareRender' +import { FormLevelErrorAlerts } from '../FormLevelErrorAlerts' + +import { SectionBody } from './SectionBody' + +import styles from '../../styles.css' + +export const Preview = (): JSX.Element => { + const { values, errors } = useFormikContext() + + return ( + + +
+ +

+ Check that the size, spacing, and shape of your wells looks correct. +

+
+
+ ) +} diff --git a/labware-library/src/labware-creator/components/Section.css b/labware-library/src/labware-creator/components/sections/SectionBody.css similarity index 100% rename from labware-library/src/labware-creator/components/Section.css rename to labware-library/src/labware-creator/components/sections/SectionBody.css diff --git a/labware-library/src/labware-creator/components/sections/SectionBody.tsx b/labware-library/src/labware-creator/components/sections/SectionBody.tsx index de6386c4307..58a23c68e42 100644 --- a/labware-library/src/labware-creator/components/sections/SectionBody.tsx +++ b/labware-library/src/labware-creator/components/sections/SectionBody.tsx @@ -1,8 +1,8 @@ import * as React from 'react' -import styles from '../Section.css' +import styles from './SectionBody.css' interface Props { - children: JSX.Element + children: React.ReactNode headingClassName?: string label: string id?: string diff --git a/labware-library/src/labware-creator/index.tsx b/labware-library/src/labware-creator/index.tsx index 8a925decd09..e049a068e1c 100644 --- a/labware-library/src/labware-creator/index.tsx +++ b/labware-library/src/labware-creator/index.tsx @@ -1,16 +1,13 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ import assert from 'assert' import Ajv from 'ajv' -import cx from 'classnames' import * as React from 'react' import { Formik } from 'formik' import { saveAs } from 'file-saver' import JSZip from 'jszip' import { reportEvent } from '../analytics' import { reportErrors } from './analyticsUtils' -import { AlertModal, PrimaryButton } from '@opentrons/components' +import { AlertModal } from '@opentrons/components' import labwareSchema from '@opentrons/shared-data/labware/schemas/2.json' -import { maskLoadName } from './fieldMasks' import { aluminumBlockAutofills, aluminumBlockChildTypeOptions, @@ -26,34 +23,31 @@ import { formLevelValidation, LabwareCreatorErrors, } from './formLevelValidation' -import { getDefaultDisplayName, getDefaultLoadName } from './formSelectors' -import { labwareTestProtocol, pipetteNameOptions } from './labwareTestProtocol' +import { labwareTestProtocol } from './labwareTestProtocol' import { fieldsToLabware } from './fieldsToLabware' import { LabwareCreator as LabwareCreatorComponent } from './components/LabwareCreator' -import { ConditionalLabwareRender } from './components/ConditionalLabwareRender' import { Dropdown } from './components/Dropdown' -import { FormLevelErrorAlerts } from './components/FormLevelErrorAlerts' import { IntroCopy } from './components/IntroCopy' -import { LinkOut } from './components/LinkOut' - -import { Section } from './components/Section' -import { TextField } from './components/TextField' import { ImportErrorModal } from './components/ImportErrorModal' import { CreateNewDefinition } from './components/sections/CreateNewDefinition' import { UploadExisting } from './components/sections/UploadExisting' import { CustomTiprackWarning } from './components/sections/CustomTiprackWarning' -import { HandPlacedTipFit } from './components/sections/HandPlacedTipFit' -import { Regularity } from './components/sections/Regularity' +import { Description } from './components/sections/Description' +import { Export } from './components/sections/Export' +import { File } from './components/sections/File' import { Footprint } from './components/sections/Footprint' -import { Height } from './components/sections/Height' import { Grid } from './components/sections/Grid' +import { GridOffset } from './components/sections/GridOffset' +import { HandPlacedTipFit } from './components/sections/HandPlacedTipFit' +import { Height } from './components/sections/Height' +import { Preview } from './components/sections/Preview' +import { Regularity } from './components/sections/Regularity' import { Volume } from './components/sections/Volume' -import { WellShapeAndSides } from './components/sections/WellShapeAndSides' import { WellBottomAndDepth } from './components/sections/WellBottomAndDepth' +import { WellShapeAndSides } from './components/sections/WellShapeAndSides' import { WellSpacing } from './components/sections/WellSpacing' -import { GridOffset } from './components/sections/GridOffset' import styles from './styles.css' @@ -67,9 +61,6 @@ import type { const ajv = new Ajv() const validateLabwareSchema = ajv.compile(labwareSchema) -const PDF_URL = - 'https://opentrons-publications.s3.us-east-2.amazonaws.com/labwareDefinition_testGuide.pdf' - export const LabwareCreator = (): JSX.Element => { const [ showExportErrorModal, @@ -326,15 +317,22 @@ export const LabwareCreator = (): JSX.Element => { > {bag => { const { - handleSubmit, values, - isValid, touched, setTouched, setValues, + isValid, + handleSubmit, } = bag const errors: LabwareCreatorErrors = bag.errors + const onExportClick = (): void => { + if (!isValid && !showExportErrorModal) { + setShowExportErrorModal(true, values) + } + handleSubmit() + } + // @ts-expect-error(IL, 2021-03-24): values/errors/touched not typed for reportErrors to be happy reportErrors({ values, errors, touched }) // TODO (ka 2019-8-27): factor out this as sub-schema from Yup schema and use it to validate instead of repeating the logic @@ -414,125 +412,21 @@ export const LabwareCreator = (): JSX.Element => {
{showCreatorForm && ( <> - {/* PAGE 1 - Labware */} - {/* PAGE 2 */} -
- -
- -

- Check that the size, spacing, and shape of your wells - looks correct. -

-
-
- - {/* PAGE 3 */} -
-
-
- -
-
- -
-
-
- {/* PAGE 4 */} - -
-
-
- - -
-
-
-
-
-
-

- Your file will be exported with a protocol that will - help you test and troubleshoot your labware definition - on the robot. The protocol requires a Single Channel - pipette on the right mount of your robot. -

-
-
- -
-
-
-
-

- Please test your definition file! -

- -

- Use the labware test protocol contained in the - downloaded file to check the accuracy of your - definition. It’s important to create definitions that - are precise and do not rely on excessive calibration - prior to each run to achieve accuracy. -

-

- Use the test guide to troubleshoot your definition. -

- - reportEvent({ - name: 'labwareCreatorClickTestLabware', - }) - } - href={PDF_URL} - className={styles.test_guide_button} - > - view test guide - -
- { - if (!isValid && !showExportErrorModal) { - setShowExportErrorModal(true, values) - } - handleSubmit() - }} - > - EXPORT FILE - -
-
+ + + + )}
diff --git a/labware-library/src/labware-creator/styles.css b/labware-library/src/labware-creator/styles.css index 3e2a8ff925d..0e3bd9de4b5 100644 --- a/labware-library/src/labware-creator/styles.css +++ b/labware-library/src/labware-creator/styles.css @@ -241,6 +241,7 @@ .export_button { margin-top: 2rem; + width: 100%; } .export_callout {