+
{is96Channel ? (
) : null}
@@ -111,7 +113,8 @@ export const MixForm = (props: StepFormProps): JSX.Element => {
{...propsForFields.aspirate_flowRate}
pipetteId={formData.pipette}
flowRateType="aspirate"
- volume={propsForFields.volume.value}
+ volume={propsForFields.volume?.value ?? 0}
+ tiprack={propsForFields.tipRack.value}
/>
{
{...propsForFields.dispense_flowRate}
pipetteId={formData.pipette}
flowRateType="dispense"
- volume={propsForFields.volume.value}
+ volume={propsForFields.volume?.value ?? 0}
+ tiprack={propsForFields.tipRack.value}
/>
diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx
index 2f0e8bdc3b0..77eaa424f36 100644
--- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx
+++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx
@@ -91,7 +91,8 @@ export const SourceDestFields = (props: SourceDestFieldsProps): JSX.Element => {
{...propsForFields[addFieldNamePrefix('flowRate')]}
pipetteId={formData.pipette}
flowRateType={prefix}
- volume={propsForFields.volume.value}
+ volume={propsForFields.volume?.value ?? 0}
+ tiprack={propsForFields.tipRack.value}
/>
{
+
{is96Channel ? (
) : null}
@@ -113,6 +115,7 @@ export const MoveLiquidForm = (props: StepFormProps): JSX.Element => {
dispense_wells={formData.dispense_wells}
pipette={formData.pipette}
volume={formData.volume}
+ tipRack={formData.tipRack}
/>
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx
index 4bc9fcec60e..b6bb1db7394 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx
@@ -178,16 +178,15 @@ function PipetteTipsField(props: PipetteTipsFieldProps): JSX.Element | null {
option.value.includes('custom_beta')
)
- const currentValue = pipettesByMount[mount].tiprackDefURI
+ const selectedValues = pipettesByMount[mount].tiprackDefURI ?? []
React.useEffect(() => {
- if (currentValue === undefined) {
- setValue(
- `pipettesByMount.${mount}.tiprackDefURI`,
- tiprackOptions[0]?.value ?? ''
- )
+ if (selectedValues.length === 0) {
+ setValue(`pipettesByMount.${mount}.tiprackDefURI`, [
+ tiprackOptions[0]?.value ?? '',
+ ])
}
- }, [currentValue, setValue, tiprackOptions])
+ }, [selectedValues, setValue, tiprackOptions])
return (
(
{
- setValue(`pipettesByMount.${mount}.tiprackDefURI`, o.value)
+ const updatedValues = selectedValues?.includes(o.value)
+ ? selectedValues.filter(value => value !== o.value)
+ : [...(selectedValues ?? []), o.value]
+ setValue(
+ `pipettesByMount.${mount}.tiprackDefURI`,
+ updatedValues.slice(0, 3)
+ )
}}
width="21.75rem"
minHeight="4rem"
+ showCheckbox
/>
))}
@@ -256,13 +262,20 @@ function PipetteTipsField(props: PipetteTipsFieldProps): JSX.Element | null {
{customTiprackOptions.map(o => (
{
- setValue(`pipettesByMount.${mount}.tiprackDefURI`, o.value)
+ const updatedValues = selectedValues?.includes(o.value)
+ ? selectedValues.filter(value => value !== o.value)
+ : [...(selectedValues ?? []), o.value]
+ setValue(
+ `pipettesByMount.${mount}.tiprackDefURI`,
+ updatedValues.slice(0, 3)
+ )
}}
width="21.75rem"
minHeight="4rem"
+ showCheckbox
/>
))}
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx
index 16f8b1f4fa1..86228712389 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx
@@ -33,7 +33,7 @@ const values = {
robotType: FLEX_ROBOT_TYPE,
},
pipettesByMount: {
- left: { pipetteName: 'mockPipetteName', tiprackDefURI: 'mocktip' },
+ left: { pipetteName: 'mockPipetteName', tiprackDefURI: ['mocktip'] },
right: { pipetteName: null, tiprackDefURI: null },
} as FormPipettesByMount,
modulesByType: {
@@ -106,7 +106,7 @@ describe('ModulesAndOtherTile', () => {
robotType: OT2_ROBOT_TYPE,
},
pipettesByMount: {
- left: { pipetteName: 'p1000_single', tiprackDefURI: 'mocktip' },
+ left: { pipetteName: 'p1000_single', tiprackDefURI: ['mocktip'] },
right: { pipetteName: null, tiprackDefURI: null },
} as FormPipettesByMount,
modulesByType: {
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTipsTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTipsTile.test.tsx
index 9995dc192e5..deab82c01d8 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTipsTile.test.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTipsTile.test.tsx
@@ -38,7 +38,7 @@ const values = {
pipettesByMount: {
left: {
pipetteName: 'p1000_single_flex',
- tiprackDefURI: 'opentrons/opentrons_flex_96_tiprack_200ul/1',
+ tiprackDefURI: ['opentrons/opentrons_flex_96_tiprack_200ul/1'],
},
right: { pipetteName: null, tiprackDefURI: null },
} as FormPipettesByMount,
@@ -150,7 +150,7 @@ describe('PipetteTipsTile', () => {
pipettesByMount: {
left: {
pipetteName: 'p10_single',
- tiprackDefURI: 'opentrons/opentrons_96_tiprack_10ul/1',
+ tiprackDefURI: ['opentrons/opentrons_96_tiprack_10ul/1'],
},
right: { pipetteName: null, tiprackDefURI: null },
} as FormPipettesByMount,
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx
index 5bd5f4f2a04..ed2242f1f87 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx
@@ -22,7 +22,7 @@ let MOCK_FORM_STATE = {
robotType: FLEX_ROBOT_TYPE,
},
pipettesByMount: {
- left: { pipetteName: 'mockPipetteName', tiprackDefURI: 'mocktip' },
+ left: { pipetteName: 'mockPipetteName', tiprackDefURI: ['mocktip'] },
right: { pipetteName: null, tiprackDefURI: null },
} as FormPipettesByMount,
modulesByType: {
diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx
index 5c4bbaed6fd..f569a4f03ce 100644
--- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx
+++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx
@@ -243,7 +243,7 @@ export function CreateFileWizard(): JSX.Element | null {
}
// auto-generate tipracks for pipettes
const newTiprackModels: string[] = uniq(
- pipettes.map(pipette => pipette.tiprackDefURI)
+ pipettes.flatMap(pipette => pipette.tiprackDefURI)
)
newTiprackModels.forEach((tiprackDefURI, index) => {
const ot2Slots = index === 0 ? '2' : '5'
@@ -353,7 +353,8 @@ const initialFormState: FormState = {
const pipetteValidationShape = Yup.object().shape({
pipetteName: Yup.string().nullable(),
- tiprackDefURI: Yup.string()
+ tiprackDefURI: Yup.array()
+ .of(Yup.string())
.nullable()
.when('pipetteName', {
is: (val: string | null): boolean => Boolean(val),
diff --git a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx
index 81f37a0dd7b..92593996844 100644
--- a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx
+++ b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx
@@ -2,7 +2,6 @@ import * as React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
Control,
- Controller,
FormState,
UseFormSetValue,
UseFormTrigger,
@@ -10,7 +9,6 @@ import {
import { useTranslation } from 'react-i18next'
import isEmpty from 'lodash/isEmpty'
import {
- DropdownField,
FormGroup,
PipetteSelect,
OutlineButton,
@@ -30,6 +28,7 @@ import { FormPipettesByMount } from '../../../step-forms'
import { getAllowAllTipracks } from '../../../feature-flags/selectors'
import { getTiprackOptions } from '../utils'
import { PipetteDiagram } from './PipetteDiagram'
+import { TiprackSelect } from './TiprackSelect'
import styles from './FilePipettesModal.module.css'
import formStyles from '../../forms/forms.module.css'
@@ -39,7 +38,7 @@ import type { ThunkDispatch } from 'redux-thunk'
import type { BaseState } from '../../../types'
import type { FormState as TypeFormState } from './index'
-export interface Props {
+export interface PipetteFieldsProps {
values: FormPipettesByMount
setValue: UseFormSetValue
trigger: UseFormTrigger
@@ -60,8 +59,8 @@ interface TiprackSelectProps {
robotType: RobotType
}
-export function PipetteFields(props: Props): JSX.Element {
- const { values, formState, setValue, trigger, control, robotType } = props
+export function PipetteFields(props: PipetteFieldsProps): JSX.Element {
+ const { values, setValue, trigger, robotType } = props
const { t } = useTranslation(['modal', 'button'])
const allowAllTipracks = useSelector(getAllowAllTipracks)
const dispatch = useDispatch>()
@@ -73,7 +72,7 @@ export function PipetteFields(props: Props): JSX.Element {
if (has96Channel) {
values.right = { pipetteName: null, tiprackDefURI: null }
}
- }, [values.left])
+ }, [has96Channel, values.left])
const renderPipetteSelect = (props: PipetteSelectProps): JSX.Element => {
const { tabIndex, mount } = props
@@ -112,43 +111,17 @@ export function PipetteFields(props: Props): JSX.Element {
allowAllTipracks: allowAllTipracks,
selectedPipetteName: selectedPipetteName,
})
- const { errors, touchedFields } = formState
- const touched =
- touchedFields.pipettesByMount &&
- touchedFields.pipettesByMount[mount] != null
-
- const tiprackDefURIError =
- errors.pipettesByMount &&
- errors.pipettesByMount[mount]?.tiprackDefURI != null
return (
- (
- ) => {
- field.onChange(e)
- trigger(`pipettesByMount.${mount}.tiprackDefURI`)
- }}
- onBlur={field.onBlur}
- />
- )}
+ {
+ // @ts-expect-error: TS can't figure out this type with react-hook-form
+ setValue(field, value)
+ trigger(`pipettesByMount.${mount}.tiprackDefURI`)
+ }}
/>
)
}
diff --git a/protocol-designer/src/components/modals/FilePipettesModal/TiprackOption.tsx b/protocol-designer/src/components/modals/FilePipettesModal/TiprackOption.tsx
new file mode 100644
index 00000000000..8908d0e7614
--- /dev/null
+++ b/protocol-designer/src/components/modals/FilePipettesModal/TiprackOption.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react'
+import {
+ Flex,
+ Text,
+ Icon,
+ DIRECTION_ROW,
+ COLORS,
+ SPACING,
+ ALIGN_CENTER,
+} from '@opentrons/components'
+
+interface TiprackOptionProps {
+ onClick: React.MouseEventHandler
+ isSelected: boolean
+ text: React.ReactNode
+}
+export function TiprackOption(props: TiprackOptionProps): JSX.Element {
+ const { text, onClick, isSelected } = props
+ return (
+
+
+ {text}
+
+ )
+}
diff --git a/protocol-designer/src/components/modals/FilePipettesModal/TiprackSelect.tsx b/protocol-designer/src/components/modals/FilePipettesModal/TiprackSelect.tsx
new file mode 100644
index 00000000000..8aa608796ec
--- /dev/null
+++ b/protocol-designer/src/components/modals/FilePipettesModal/TiprackSelect.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react'
+import { Flex, DIRECTION_COLUMN } from '@opentrons/components'
+import { TiprackOption } from './TiprackOption'
+import type { Mount } from '@opentrons/components'
+import type { FormPipettesByMount } from '../../../step-forms'
+import type { TiprackOption as TiprackOptionType } from '../utils'
+
+interface TiprackSelectProps {
+ mount: Mount
+ tiprackOptions: TiprackOptionType[]
+ onSetFieldValue: (field: string, value: any) => void
+ values: FormPipettesByMount
+}
+export const TiprackSelect = (
+ props: TiprackSelectProps
+): JSX.Element | null => {
+ const { mount, tiprackOptions, values, onSetFieldValue } = props
+ const selectedPipetteName = values[mount].pipetteName
+
+ let selectedValues = values[mount].tiprackDefURI ?? []
+
+ React.useEffect(() => {
+ if (selectedValues?.length === 0 && tiprackOptions.length > 0) {
+ selectedValues = [tiprackOptions[0].value]
+ onSetFieldValue(`pipettesByMount.${mount}.tiprackDefURI`, selectedValues)
+ }
+ }, [selectedValues, onSetFieldValue, tiprackOptions])
+
+ if (selectedPipetteName == null) return null
+
+ return (
+
+ {tiprackOptions.map(option => (
+ {
+ const updatedValues = selectedValues?.includes(option.value)
+ ? selectedValues.filter(value => value !== option.value)
+ : [...(selectedValues ?? []), option.value]
+ onSetFieldValue(
+ `pipettesByMount.${mount}.tiprackDefURI`,
+ updatedValues.slice(0, 3)
+ )
+ }}
+ />
+ ))}
+
+ )
+}
diff --git a/protocol-designer/src/components/modals/FilePipettesModal/__tests__/TiprackOptions.test.tsx b/protocol-designer/src/components/modals/FilePipettesModal/__tests__/TiprackOptions.test.tsx
new file mode 100644
index 00000000000..6b9004a2472
--- /dev/null
+++ b/protocol-designer/src/components/modals/FilePipettesModal/__tests__/TiprackOptions.test.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react'
+import { vi, describe, beforeEach, it, expect } from 'vitest'
+import { screen } from '@testing-library/react'
+import { renderWithProviders } from '../../../../__testing-utils__'
+import { COLORS } from '@opentrons/components'
+import { TiprackOption } from '../TiprackOption'
+
+const render = (props: React.ComponentProps) => {
+ return renderWithProviders()[0]
+}
+
+describe('TiprackOption', () => {
+ let props: React.ComponentProps
+ beforeEach(() => {
+ props = {
+ onClick: vi.fn(),
+ isSelected: true,
+ text: 'mockText',
+ }
+ })
+ it('renders a selected tiprack option', () => {
+ render(props)
+ screen.getByText('mockText')
+ expect(screen.getByLabelText('TiprackOption_checkbox-marked')).toHaveStyle(
+ `color: ${COLORS.blue50}`
+ )
+ })
+ it('renders an unselected tiprack option', () => {
+ props.isSelected = false
+ render(props)
+ screen.getByText('mockText')
+ expect(
+ screen.getByLabelText('TiprackOption_checkbox-blank-outline')
+ ).toHaveStyle(`color: ${COLORS.grey50}`)
+ })
+})
diff --git a/protocol-designer/src/components/modals/FilePipettesModal/__tests__/TiprackSelect.test.tsx b/protocol-designer/src/components/modals/FilePipettesModal/__tests__/TiprackSelect.test.tsx
new file mode 100644
index 00000000000..bf1f5165002
--- /dev/null
+++ b/protocol-designer/src/components/modals/FilePipettesModal/__tests__/TiprackSelect.test.tsx
@@ -0,0 +1,39 @@
+import * as React from 'react'
+import { vi, describe, beforeEach, it, expect } from 'vitest'
+import { screen } from '@testing-library/react'
+import { renderWithProviders } from '../../../../__testing-utils__'
+import { TiprackSelect } from '../TiprackSelect'
+import { TiprackOption } from '../TiprackOption'
+
+vi.mock('../TiprackOption')
+
+const render = (props: React.ComponentProps) => {
+ return renderWithProviders()[0]
+}
+
+describe('TiprackSelect', () => {
+ let props: React.ComponentProps
+ beforeEach(() => {
+ vi.mocked(TiprackOption).mockReturnValue(mock TiprackOption
)
+ props = {
+ mount: 'left',
+ tiprackOptions: [
+ { name: 'mockTip', value: 'mockUri' },
+ { name: 'mockTip2', value: 'mockUri2' },
+ { name: 'mockTip3', value: 'mockUri3' },
+ ],
+ onSetFieldValue: vi.fn(),
+ values: {
+ left: {
+ pipetteName: 'mockPipetteName',
+ tiprackDefURI: ['mockUri', 'mockUri2'],
+ },
+ right: { pipetteName: null, tiprackDefURI: null },
+ },
+ }
+ })
+ it('renders 3 options in tiprack option', () => {
+ render(props)
+ expect(screen.getAllByText('mock TiprackOption')).toHaveLength(3)
+ })
+})
diff --git a/protocol-designer/src/components/modals/FilePipettesModal/index.tsx b/protocol-designer/src/components/modals/FilePipettesModal/index.tsx
index b08b6f5d05e..474cbd59287 100644
--- a/protocol-designer/src/components/modals/FilePipettesModal/index.tsx
+++ b/protocol-designer/src/components/modals/FilePipettesModal/index.tsx
@@ -119,7 +119,8 @@ const initialFormState: FormState = {
const pipetteValidationShape = Yup.object().shape({
pipetteName: Yup.string().nullable(),
- tiprackDefURI: Yup.string()
+ tiprackDefURI: Yup.array()
+ .of(Yup.string())
.nullable()
.when('pipetteName', {
is: (val: string | null): boolean => Boolean(val),
@@ -172,7 +173,7 @@ const makeUpdatePipettes = (
[pipetteId: string]: {
mount: string
name: PipetteName
- tiprackDefURI: string
+ tiprackDefURI: string[]
id: string
}
} = {}
@@ -253,9 +254,14 @@ const makeUpdatePipettes = (
nextPipettes,
(nextPipette: typeof nextPipettes[keyof typeof nextPipettes]) => {
const newPipetteId = nextPipette.id
+ const nextTips = nextPipette.tiprackDefURI
+ const oldTips =
+ newPipetteId in prevPipettes
+ ? prevPipettes[newPipetteId].tiprackDefURI
+ : null
const tiprackChanged =
- newPipetteId in prevPipettes &&
- nextPipette.tiprackDefURI !== prevPipettes[newPipetteId].tiprackDefURI
+ oldTips != null &&
+ nextTips.every((item, index) => item !== oldTips[index])
return tiprackChanged
}
).map(pipette => pipette.id)
@@ -360,8 +366,8 @@ export const FilePipettesModal = (props: Props): JSX.Element => {
) // this is mostly for flow
// @ts-expect-error(sa, 2021-6-21): TODO validate that pipette names coming from the modal are actually valid pipette names on PipetteName type
return formPipette &&
- formPipette.pipetteName &&
- formPipette.tiprackDefURI &&
+ formPipette.pipetteName != null &&
+ formPipette.tiprackDefURI != null &&
(mount === 'left' || mount === 'right')
? [
...acc,
@@ -512,7 +518,7 @@ export const FilePipettesModal = (props: Props): JSX.Element => {
@@ -524,7 +530,7 @@ export const FilePipettesModal = (props: Props): JSX.Element => {
{showEditPipetteConfirmation ? (
setShowEditPipetteConfirmation(false)}
- onConfirm={handleSubmit(handleFormSubmit)}
+ onConfirm={() => handleSubmit(handleFormSubmit)()}
/>
) : null}
diff --git a/protocol-designer/src/file-data/__fixtures__/createFile/commonFields.ts b/protocol-designer/src/file-data/__fixtures__/createFile/commonFields.ts
index 046d07cff48..3671654514a 100644
--- a/protocol-designer/src/file-data/__fixtures__/createFile/commonFields.ts
+++ b/protocol-designer/src/file-data/__fixtures__/createFile/commonFields.ts
@@ -57,8 +57,8 @@ export const pipetteEntities: PipetteEntities = {
id: 'pipetteId',
name: 'p10_single',
spec: fixtureP10SingleV2Specs,
- tiprackDefURI: 'opentrons/opentrons_96_tiprack_10ul/1',
- tiprackLabwareDef: fixtureTiprack10ul,
+ tiprackDefURI: ['opentrons/opentrons_96_tiprack_10ul/1'],
+ tiprackLabwareDef: [fixtureTiprack10ul],
},
}
export const labwareNicknamesById: Record