Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Protocol-designer, components, step-generation): multi tiprack support #13166

Closed
wants to merge 12 commits into from
Prev Previous commit
Next Next commit
add an error creator for no tip rack selected and fix tests
  • Loading branch information
jerader committed Jul 25, 2023
commit 894fdebf0538dcddcd6cc66a68ea48a7c3118adc
2 changes: 1 addition & 1 deletion components/src/forms/DropdownField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface DropdownFieldProps {
/** blur handler */
onBlur?: React.FocusEventHandler<HTMLSelectElement>
/** value that is selected */
value?: string | null | undefined | string
value?: string | null | undefined
/** optional id for the <select> element */
id?: string
/** name of field in form */
Expand Down
9 changes: 6 additions & 3 deletions components/src/instrument/InfoItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import styles from './instrument.css'
export interface InfoItemProps {
title: string | null
value: string
className?: string
}

/**
* Used by `InstrumentInfo` for its titled values.
* But if you're using this, you probably want `LabeledValue` instead.
*/
export function InfoItem(props: InfoItemProps): JSX.Element {
const { title, value } = props
const { title, value, className } = props

return (
<div>
<div className={className}>
{title != null ? <h2 className={styles.title}>{title}</h2> : null}
<span className={styles.value}>{value}</span>
<span className={styles.value} style={{ paddingBottom: '4px' }}>
{value}
</span>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
}

.large_field {
width: 12rem;
width: 13rem;
}

.small_field {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ function PipetteTipsField(props: PipetteTipsFieldProps): JSX.Element | null {
const updatedValues = selectedValues?.includes(o.value)
? selectedValues.filter(value => value !== o.value)
: [...(selectedValues ?? []), o.value]
setFieldValue(nameAccessor, updatedValues)
setFieldValue(nameAccessor, updatedValues.slice(0, 3))
}}
width="21.75rem"
minHeight="4rem"
Expand Down Expand Up @@ -262,7 +262,7 @@ function PipetteTipsField(props: PipetteTipsFieldProps): JSX.Element | null {
const updatedValues = selectedValues?.includes(o.value)
? selectedValues.filter(value => value !== o.value)
: [...(selectedValues ?? []), o.value]
setFieldValue(nameAccessor, updatedValues)
setFieldValue(nameAccessor, updatedValues.slice(0, 3))
}}
width="21.75rem"
minHeight="4rem"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export function TiprackOption(props: TiprackOptionProps): JSX.Element {
size="1.25rem"
name={isSelected ? 'checkbox-marked' : 'checkbox-blank-outline'}
/>
{/* note: fontSize 12 isn't in the design system but it is to match
other font sizes in the modal this is in **/}
<Text fontSize="12px">{text}</Text>
</Flex>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const TiprackSelect = (
const updatedValues = selectedValues?.includes(option.value)
? selectedValues.filter(value => value !== option.value)
: [...(selectedValues ?? []), option.value]
onSetFieldValue(nameAccessor, updatedValues)
onSetFieldValue(nameAccessor, updatedValues.slice(0, 3))
}}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { ChangeEvent } from 'react'

Check failure on line 1 in protocol-designer/src/components/modals/FilePipettesModal/__tests__/PipetteFields.test.tsx

View workflow job for this annotation

GitHub Actions / js checks

'ChangeEvent' is defined but never used
import { Provider } from 'react-redux'
import { mount } from 'enzyme'
import {
PipetteSelect,
DropdownField,

Check failure on line 6 in protocol-designer/src/components/modals/FilePipettesModal/__tests__/PipetteFields.test.tsx

View workflow job for this annotation

GitHub Actions / js checks

'DropdownField' is defined but never used
OutlineButton,
} from '@opentrons/components'
import { OT2_ROBOT_TYPE } from '@opentrons/shared-data'
Expand All @@ -20,6 +20,7 @@

import type { ActionMeta } from 'react-select'
import type { SelectOption } from '@opentrons/components'
import { TiprackSelect } from '../TiprackSelect'

jest.mock('../../../../feature-flags/selectors')
jest.mock('../../../../labware-defs/selectors')
Expand All @@ -44,7 +45,7 @@
const leftTiprackKey = `${leftPipetteKey}.tiprackDefURI`
const leftPipetteName = `${leftPipetteKey}.pipetteName`
const rightPipetteKey = 'pipettesByMount.left'
const rightTiprackKey = `${rightPipetteKey}.tiprackDefURI`

Check failure on line 48 in protocol-designer/src/components/modals/FilePipettesModal/__tests__/PipetteFields.test.tsx

View workflow job for this annotation

GitHub Actions / js checks

'rightTiprackKey' is assigned a value but never used
const unselectedPipette = {
pipetteName: '',
tiprackDefURI: [''],
Expand Down Expand Up @@ -95,25 +96,15 @@
)
}

it('renders a selection for left and right pipette with disabled tiprack select', () => {
it('renders a selection for left and right pipette with no tiprack select', () => {
props.values.left = unselectedPipette
props.values.right = unselectedPipette

const wrapper = render(props)

expect(wrapper.find(PipetteSelect)).toHaveLength(2)
expect(
wrapper
.find(DropdownField)
.filter({ name: leftTiprackKey })
.prop('disabled')
).toBe(true)
expect(
wrapper
.find(DropdownField)
.filter({ name: rightTiprackKey })
.prop('disabled')
).toBe(true)
expect(wrapper.find(TiprackSelect)).toHaveLength(2)
expect(wrapper.find(TiprackSelect)).toEqual({})
})

it('selects a pipette and clears tiprack fields that has been touched', () => {
Expand All @@ -134,35 +125,27 @@
])
})

it('undisables tiprack selection when a pipette is selected', () => {
it('shows tiprack selection when a pipette is selected and selects the first option', () => {
props.values.left.tiprackDefURI = ['']

const wrapper = render(props)

expect(
wrapper
.find(DropdownField)
.filter({ name: leftTiprackKey })
.prop('disabled')
).toBe(false)
const tiprackSelectValues = wrapper.find(TiprackSelect).at(0).prop('values')
expect(tiprackSelectValues).toEqual({
left: { pipetteName: 'p300', tiprackDefURI: [''] },
right: { pipetteName: 'p1000', tiprackDefURI: ['tiprack_1000'] },
})
})

it('selects a tiprack for the pipette', () => {
props.values.left.tiprackDefURI = ['']
const event = {
target: {
name: leftTiprackKey,
value: 'tiprack_300',
},
}

const wrapper = render(props)
const leftTiprackSelect = wrapper
.find(DropdownField)
.filter({ name: leftTiprackKey })
leftTiprackSelect.prop('onChange')(event as ChangeEvent<HTMLSelectElement>)
const leftTiprackSelect = wrapper.find(TiprackSelect).at(1)
leftTiprackSelect.prop(
'onSetFieldValue'
)('pipettesByMount.left.tiprackDefURI', ['tiprack_1000'])

expect(props.onFieldChange).toHaveBeenCalledWith(event)
expect(props.onSetFieldValue).toHaveBeenCalled()
})
it('displays pipette diagrams for selected pipettes', () => {
const wrapper = render(props)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react'
import { renderWithProviders } from '@opentrons/components'
import { TiprackSelect } from '../TiprackSelect'
import { TiprackOption } from '../TiprackOption'

jest.mock('../TiprackOption')

const mockTiprackOption = TiprackOption as jest.MockedFunction<
typeof TiprackOption
>

const render = (props: React.ComponentProps<typeof TiprackSelect>) => {
return renderWithProviders(<TiprackSelect {...props} />)[0]
}

describe('TiprackSelect', () => {
let props: React.ComponentProps<typeof TiprackSelect>
beforeEach(() => {
mockTiprackOption.mockReturnValue(<div>mock TiprackOption</div>)
props = {
mount: 'left',
tiprackOptions: [
{ name: 'mockTip', value: 'mockUri' },
{ name: 'mockTip2', value: 'mockUri2' },
{ name: 'mockTip3', value: 'mockUri3' },
],
onSetFieldValue: jest.fn(),
values: {
left: {
pipetteName: 'mockPipetteName',
tiprackDefURI: ['mockUri', 'mockUri2'],
},
right: { pipetteName: null, tiprackDefURI: null },
},
}
})
it('renders 3 options in tiprack option', () => {
const { getAllByText } = render(props)
expect(getAllByText('mock TiprackOption')).toHaveLength(3)
})
})
4 changes: 4 additions & 0 deletions protocol-designer/src/localization/en/alert.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@
"title": "Not enough tips to complete action",
"body": "Add another tip rack to an empty slot in "
},
"NO_TIP_SELECTED": {
"title": "No tip rack was selected to complete action",
"body": "Add a tip rack in the step"
},
"NO_TIP_ON_PIPETTE": {
"title": "No tip on pipette at the start of step",
"body1": "Choose a different Change Tip setting. Change Tip cannot be \"Never\" the first time a pipette is used in a protocol, or following a step that used the ",
Expand Down
10 changes: 9 additions & 1 deletion step-generation/src/commandCreators/atomic/replaceTip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const _pickUpTip: CommandCreator<PickUpTipArgs> = (

interface ReplaceTipArgs {
pipette: string
tipRack: string
tipRack: string | null
}

/**
Expand All @@ -54,12 +54,20 @@ export const replaceTip: CommandCreator<ReplaceTipArgs> = (
prevRobotState
) => {
const { pipette, tipRack } = args

if (tipRack == null) {
return {
errors: [errorCreators.noTipSelected()],
}
}

const nextTiprack = getNextTiprack(
pipette,
tipRack,
invariantContext,
prevRobotState
)

if (nextTiprack == null) {
// no valid next tip / tiprack, bail out
return {
Expand Down
7 changes: 7 additions & 0 deletions step-generation/src/errorCreators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export function insufficientTips(): CommandCreatorError {
}
}

export function noTipSelected(): CommandCreatorError {
return {
type: 'NO_TIP_SELECTED',
message: 'No tips were selected for this step',
}
}

export function noTipOnPipette(args: {
actionName: string
pipette: string
Expand Down
1 change: 0 additions & 1 deletion step-generation/src/getNextRobotStateAndWarnings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ function _getNextRobotStateAndWarningsSingleCommand(
break

case 'pickUpTip':
console.log('tiprack id for pick up tip', command.params.labwareId)
forPickUpTip(command.params, invariantContext, robotStateAndWarnings)
break

Expand Down
1 change: 1 addition & 0 deletions step-generation/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ export type ErrorType =
| 'HEATER_SHAKER_NORTH_SOUTH__OF_NON_TIPRACK_WITH_MULTI_CHANNEL'
| 'HEATER_SHAKER_LATCH_CLOSED'
| 'LABWARE_OFF_DECK'
| 'NO_TIP_SELECTED'

export interface CommandCreatorError {
message: string
Expand Down
Loading