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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol-designer, step-generation, shared-data): 96-channel tip adapter + well selection support #13637

Merged
merged 19 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is for labware creator, i defaulted to only having the 8-channel since it doesn't support creating labware for a 96-channel yet.


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 {
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 @@
/** 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 @@
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 @@
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 @@
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 @@
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)}

Check warning on line 248 in protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx

View check run for this annotation

Codecov / codecov/patch

protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx#L248

Added line #L248 was not covered by tests
// @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy)
onMouseLeave={() => setPreviewedLabware()}

Check warning on line 250 in protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx

View check run for this annotation

Codecov / codecov/patch

protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx#L250

Added line #L250 was not covered by tests
/>
) : null
}

const customLabwareURIs: string[] = React.useMemo(
() => Object.keys(customLabwareDefs),
[customLabwareDefs]
Expand All @@ -239,13 +265,10 @@
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 @@
moduleCompatibility = 'notCompatible'
}
}

return (
<>
<Portal>
Expand Down Expand Up @@ -420,37 +444,29 @@
})
) : (
<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

Check warning on line 465 in protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx

View check run for this annotation

Codecov / codecov/patch

protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx#L464-L465

Added lines #L464 - L465 were not covered by tests
)
return getLabwareAdapterItem(index, labwareDefUri)

Check warning on line 467 in protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx

View check run for this annotation

Codecov / codecov/patch

protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx#L467

Added line #L467 was not covered by tests
}
)}
</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')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,27 @@
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)

Check warning on line 39 in protocol-designer/src/components/LabwareSelectionModal/index.ts

View check run for this annotation

Codecov / codecov/patch

protocol-designer/src/components/LabwareSelectionModal/index.ts#L38-L39

Added lines #L38 - L39 were not covered by tests

// 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 @@
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
Loading