Skip to content

Commit

Permalink
feat(protocol-designer, step-generation, shared-data): 96-channel tip…
Browse files Browse the repository at this point in the history
… adapter + well selection support (#13637)

closes RAUT-678 RAUT-679 RAUT-697 RAUT-755
  • Loading branch information
jerader authored Oct 25, 2023
1 parent b37f9f8 commit 98716d8
Show file tree
Hide file tree
Showing 46 changed files with 688 additions and 256 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('determineMultiChannelSupport', () => {
it('should allow multi channel when getWellNamePerMultiTip returns 8 wells', () => {
const def: any = 'fakeDef'
when(getWellNamePerMultiTipMock)
.calledWith(def, 'A1')
.calledWith(def, 'A1', 8)
.mockReturnValue(['A1', 'B1', 'C1', 'D1', 'E1', 'F1', 'G1', 'H1'])
const result = determineMultiChannelSupport(def)
expect(result).toEqual({
Expand All @@ -37,7 +37,9 @@ describe('determineMultiChannelSupport', () => {

it('should NOT allow multi channel when getWellNamePerMultiTip does not return 8 wells', () => {
const def: any = 'fakeDef'
when(getWellNamePerMultiTipMock).calledWith(def, 'A1').mockReturnValue(null)
when(getWellNamePerMultiTipMock)
.calledWith(def, 'A1', 8)
.mockReturnValue(null)
const result = determineMultiChannelSupport(def)
expect(result).toEqual({
disablePipetteField: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export const determineMultiChannelSupport = (

// allow multichannel pipette options only if
// all 8 channels fit into the first column correctly
// TODO(Jr, 9/25/23): support 96-channel in labware creator then plug in
// channels below in getWellNamePerMultiTip
const multiChannelTipsFirstColumn =
def !== null ? getWellNamePerMultiTip(def, 'A1') : null
def !== null ? getWellNamePerMultiTip(def, 'A1', 8) : null

const allowMultiChannel =
multiChannelTipsFirstColumn !== null &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import styles from './LabwareOverlays.css'

interface LabwareControlsProps {
labwareOnDeck: LabwareOnDeck
selectedTerminalItemId?: TerminalItemId | null
slot: DeckSlot
setHoveredLabware: (labware?: LabwareOnDeck | null) => unknown
setDraggedLabware: (labware?: LabwareOnDeck | null) => unknown
swapBlocked: boolean
selectedTerminalItemId?: TerminalItemId | null
}

export const LabwareControls = (props: LabwareControlsProps): JSX.Element => {
Expand All @@ -30,6 +30,7 @@ export const LabwareControls = (props: LabwareControlsProps): JSX.Element => {
setDraggedLabware,
swapBlocked,
} = props

const canEdit = selectedTerminalItemId === START_TERMINAL_ITEM_ID
const [x, y] = slot.position
const width = labwareOnDeck.def.dimensions.xDimension
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import assert from 'assert'
import * as React from 'react'
import { Icon, RobotCoordsForeignDiv } from '@opentrons/components'
import { DropTarget, DropTargetConnector, DropTargetMonitor } from 'react-dnd'
import cx from 'classnames'
import { connect } from 'react-redux'
import noop from 'lodash/noop'
import { DropTarget, DropTargetConnector, DropTargetMonitor } from 'react-dnd'
import cx from 'classnames'
import { Icon, RobotCoordsForeignDiv } from '@opentrons/components'
import { i18n } from '../../../localization'
import { DND_TYPES } from '../../../constants'
import {
getLabwareIsCompatible,
getLabwareIsCustom,
} from '../../../utils/labwareModuleCompatibility'
import { BlockedSlot } from './BlockedSlot'
import {
moveDeckItem,
openAddLabwareModal,
Expand All @@ -21,13 +20,14 @@ import {
selectors as labwareDefSelectors,
} from '../../../labware-defs'
import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist'
import { BlockedSlot } from './BlockedSlot'

import { BaseState, DeckSlot, ThunkDispatch } from '../../../types'
import { LabwareOnDeck } from '../../../step-forms'
import {
import type {
DeckSlot as DeckSlotDefinition,
ModuleType,
} from '@opentrons/shared-data'
import type { BaseState, DeckSlot, ThunkDispatch } from '../../../types'
import type { LabwareOnDeck } from '../../../step-forms'
import styles from './LabwareOverlays.css'

interface DNDP {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SPAN7_8_10_11_SLOT } from '../../constants'
import {
getLabwareIsCompatible as _getLabwareIsCompatible,
getLabwareCompatibleWithAdapter,
ADAPTER_96_CHANNEL,
} from '../../utils/labwareModuleCompatibility'
import { getOnlyLatestDefs } from '../../labware-defs/utils'
import { Portal } from '../portals/TopPortal'
Expand All @@ -51,6 +52,7 @@ export interface Props {
/** tipracks that may be added to deck (depends on pipette<>tiprack assignment) */
permittedTipracks: string[]
isNextToHeaterShaker: boolean
has96Channel: boolean
adapterLoadName?: string
}

Expand Down Expand Up @@ -121,8 +123,10 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
selectLabware,
isNextToHeaterShaker,
adapterLoadName,
has96Channel,
} = props
const defs = getOnlyLatestDefs()
const URIs = Object.keys(defs)
const [selectedCategory, setSelectedCategory] = React.useState<string | null>(
null
)
Expand Down Expand Up @@ -190,7 +194,8 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
const smallYDimension = labwareDef.dimensions.yDimension < 85.48
const irregularSize = smallXDimension && smallYDimension
const adapter = labwareDef.metadata.displayCategory === 'adapter'

const isAdapter96Channel =
labwareDef.parameters.loadName === ADAPTER_96_CHANNEL
return (
(filterRecommended &&
!getLabwareIsRecommended(labwareDef, moduleType)) ||
Expand All @@ -200,7 +205,10 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM
)) ||
!getLabwareCompatible(labwareDef) ||
(adapter && irregularSize && !slot?.includes(HEATERSHAKER_MODULE_TYPE))
(adapter &&
irregularSize &&
!slot?.includes(HEATERSHAKER_MODULE_TYPE)) ||
(isAdapter96Channel && !has96Channel)
)
},
[filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot]
Expand All @@ -226,6 +234,24 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
return `Slot ${slot} Labware`
}

const getLabwareAdapterItem = (
index: number,
labwareDefUri?: string
): JSX.Element | null => {
const labwareDef = labwareDefUri != null ? defs[labwareDefUri] : null
return labwareDef != null ? (
<LabwareItem
key={`${labwareDef.parameters.loadName}_${index}`}
icon="check-decagram"
labwareDef={labwareDef}
selectLabware={selectLabware}
onMouseEnter={() => setPreviewedLabware(labwareDef)}
// @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy)
onMouseLeave={() => setPreviewedLabware()}
/>
) : null
}

const customLabwareURIs: string[] = React.useMemo(
() => Object.keys(customLabwareDefs),
[customLabwareDefs]
Expand All @@ -239,13 +265,10 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
defs,
(acc, def: typeof defs[keyof typeof defs]) => {
const category: string = def.metadata.displayCategory
// filter out non-permitted tipracks +
// temporarily filtering out 96-channel adapter until we support
// 96-channel
// filter out non-permitted tipracks
if (
(category === 'tipRack' &&
!permittedTipracks.includes(getLabwareDefURI(def))) ||
def.parameters.loadName === 'opentrons_flex_96_tiprack_adapter'
category === 'tipRack' &&
!permittedTipracks.includes(getLabwareDefURI(def))
) {
return acc
}
Expand Down Expand Up @@ -343,6 +366,7 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
moduleCompatibility = 'notCompatible'
}
}

return (
<>
<Portal>
Expand Down Expand Up @@ -420,37 +444,29 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
})
) : (
<PDTitledList
data-testid="LabwareSelectionModal_adapterCompatibleLabware"
key={adapterCompatibleLabware}
title="adapter compatible labware"
collapsed={selectedCategory !== adapterCompatibleLabware}
onCollapseToggle={makeToggleCategory(adapterCompatibleLabware)}
onClick={makeToggleCategory(adapterCompatibleLabware)}
inert={false}
>
{getLabwareCompatibleWithAdapter(adapterLoadName).map(
(adapterDefUri, index) => {
const latestDefs = getOnlyLatestDefs()
const Uris = Object.keys(latestDefs)
const labwareDefUri = Uris.find(
defUri => defUri === adapterDefUri
)
const labwareDef = labwareDefUri
? latestDefs[labwareDefUri]
: null

return labwareDef != null ? (
<LabwareItem
key={index}
icon="check-decagram"
labwareDef={labwareDef}
selectLabware={selectLabware}
onMouseEnter={() => setPreviewedLabware(labwareDef)}
// @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy)
onMouseLeave={() => setPreviewedLabware()}
/>
) : null
}
)}
{has96Channel && adapterLoadName === ADAPTER_96_CHANNEL
? permittedTipracks.map((tiprackDefUri, index) => {
const labwareDefUri = URIs.find(
defUri => defUri === tiprackDefUri
)
return getLabwareAdapterItem(index, labwareDefUri)
})
: getLabwareCompatibleWithAdapter(adapterLoadName).map(
(adapterDefUri, index) => {
const labwareDefUri = URIs.find(
defUri => defUri === adapterDefUri
)
return getLabwareAdapterItem(index, labwareDefUri)
}
)}
</PDTitledList>
)}
</ul>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import * as React from 'react'
import i18next from 'i18next'
import { renderWithProviders } from '@opentrons/components'
import { renderWithProviders, nestedTextMatcher } from '@opentrons/components'
import {
getIsLabwareAboveHeight,
MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM,
} from '@opentrons/shared-data'
import {
ADAPTER_96_CHANNEL,
getLabwareCompatibleWithAdapter,
} from '../../../utils/labwareModuleCompatibility'
import { Portal } from '../../portals/TopPortal'
import { LabwareSelectionModal } from '../LabwareSelectionModal'

jest.mock('../../../utils/labwareModuleCompatibility')
jest.mock('../../portals/TopPortal')
jest.mock('../../Hints/useBlockingHint')
jest.mock('@opentrons/shared-data', () => {
const actualSharedData = jest.requireActual('@opentrons/shared-data')
Expand All @@ -19,7 +26,10 @@ jest.mock('@opentrons/shared-data', () => {
const mockGetIsLabwareAboveHeight = getIsLabwareAboveHeight as jest.MockedFunction<
typeof getIsLabwareAboveHeight
>

const mockPortal = Portal as jest.MockedFunction<typeof Portal>
const mockGetLabwareCompatibleWithAdapter = getLabwareCompatibleWithAdapter as jest.MockedFunction<
typeof getLabwareCompatibleWithAdapter
>
const render = (props: React.ComponentProps<typeof LabwareSelectionModal>) => {
return renderWithProviders(<LabwareSelectionModal {...props} />, {
i18nInstance: i18next,
Expand All @@ -36,7 +46,10 @@ describe('LabwareSelectionModal', () => {
customLabwareDefs: {},
permittedTipracks: [],
isNextToHeaterShaker: false,
has96Channel: false,
}
mockPortal.mockReturnValue(<div>mock portal</div>)
mockGetLabwareCompatibleWithAdapter.mockReturnValue([])
})
it('should NOT filter out labware above 57 mm when the slot is NOT next to a heater shaker', () => {
props.isNextToHeaterShaker = false
Expand All @@ -51,4 +64,15 @@ describe('LabwareSelectionModal', () => {
MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM
)
})
it('should display only permitted tipracks if the 96-channel is attached', () => {
const mockTipUri = 'fixture/fixture_tiprack_1000_ul/1'
const mockPermittedTipracks = [mockTipUri]
props.slot = 'A2'
props.has96Channel = true
props.adapterLoadName = ADAPTER_96_CHANNEL
props.permittedTipracks = mockPermittedTipracks
const { getByText } = render(props)
getByText(nestedTextMatcher('adapter compatible labware')).click()
getByText('Opentrons GEB 1000uL Tiprack')
})
})
12 changes: 11 additions & 1 deletion protocol-designer/src/components/LabwareSelectionModal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,27 @@ import {
selectors as labwareDefSelectors,
} from '../../labware-defs'
import { selectors as stepFormSelectors, ModuleOnDeck } from '../../step-forms'
import { BaseState, ThunkDispatch } from '../../types'
import { getHas96Channel } from '../../utils'
import { getPipetteEntities } from '../../step-forms/selectors'
import { adapter96ChannelDefUri } from '../modals/CreateFileWizard'
import type { BaseState, ThunkDispatch } from '../../types'
interface SP {
customLabwareDefs: LabwareSelectionModalProps['customLabwareDefs']
slot: LabwareSelectionModalProps['slot']
parentSlot: LabwareSelectionModalProps['parentSlot']
moduleType: LabwareSelectionModalProps['moduleType']
permittedTipracks: LabwareSelectionModalProps['permittedTipracks']
isNextToHeaterShaker: boolean
has96Channel: boolean
adapterDefUri?: string
adapterLoadName?: string
}

function mapStateToProps(state: BaseState): SP {
const slot = labwareIngredSelectors.selectedAddLabwareSlot(state) || null
const pipettes = getPipetteEntities(state)
const has96Channel = getHas96Channel(pipettes)

// TODO: Ian 2019-10-29 needs revisit to support multiple manualIntervention steps
const modulesById = stepFormSelectors.getInitialDeckSetup(state).modules
const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map(
Expand Down Expand Up @@ -57,6 +65,8 @@ function mapStateToProps(state: BaseState): SP {
parentSlot,
moduleType,
isNextToHeaterShaker,
has96Channel,
adapterDefUri: has96Channel ? adapter96ChannelDefUri : undefined,
permittedTipracks: stepFormSelectors.getPermittedTipracks(state),
adapterLoadName: adapterLoadNameOnDeck,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface DP {

export type OP = FieldProps & {
primaryWellCount?: number
isMulti?: boolean | null
is8Channel?: boolean | null
pipetteId?: string | null
labwareId?: string | null
}
Expand Down Expand Up @@ -64,7 +64,7 @@ export class WellSelectionInputComponent extends React.Component<Props> {

render(): JSX.Element {
const modalKey = this.getModalKey()
const label = this.props.isMulti
const label = this.props.is8Channel
? i18n.t('form.step_edit_form.wellSelectionLabel.columns')
: i18n.t('form.step_edit_form.wellSelectionLabel.wells')
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type OP = FieldProps & {
pipetteId?: string | null
}
interface SP {
isMulti: Props['isMulti']
is8Channel: Props['is8Channel']
primaryWellCount: Props['primaryWellCount']
}

Expand All @@ -29,12 +29,12 @@ const mapStateToProps = (state: BaseState, ownProps: OP): SP => {
const selectedWells = ownProps.value
const pipette =
pipetteId && stepFormSelectors.getPipetteEntities(state)[pipetteId]
const isMulti = pipette ? pipette.spec.channels > 1 : false
const is8Channel = pipette ? pipette.spec.channels === 8 : false
return {
primaryWellCount: Array.isArray(selectedWells)
? selectedWells.length
: undefined,
isMulti,
is8Channel,
}
}

Expand All @@ -53,7 +53,7 @@ function mergeProps(stateProps: SP, _dispatchProps: null, ownProps: OP): Props {
return {
disabled,
errorToShow,
isMulti: stateProps.isMulti,
is8Channel: stateProps.is8Channel,
labwareId,
name,
onFieldBlur,
Expand Down
Loading

0 comments on commit 98716d8

Please sign in to comment.