Skip to content

Commit

Permalink
feat(protocol-designer): allow custom labware on modules (#5175)
Browse files Browse the repository at this point in the history
* feat(protocol-designer): allow custom labware on modules on deck

closes #4910

* add tests

* clean up files and tests

* add more tests
  • Loading branch information
jen-fong authored Mar 12, 2020
1 parent 65425da commit 1c4e1b5
Show file tree
Hide file tree
Showing 6 changed files with 441 additions and 20 deletions.
41 changes: 35 additions & 6 deletions protocol-designer/src/components/DeckSetup/DeckSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
import { getDeckDefinitions } from '@opentrons/components/src/deck/getDeckDefinitions'
import { PSEUDO_DECK_SLOTS, GEN_ONE_MULTI_PIPETTES } from '../../constants'
import type { TerminalItemId } from '../../steplist'
import { getLabwareIsCompatible } from '../../utils/labwareModuleCompatibility'
import {
getLabwareIsCompatible,
getLabwareIsCustom,
} from '../../utils/labwareModuleCompatibility'
import { selectors as labwareDefSelectors } from '../../labware-defs'
import {
getModuleVizDims,
inferModuleOrientationFromSlot,
Expand All @@ -36,6 +40,7 @@ import type {
LabwareOnDeck as LabwareOnDeckType,
ModuleOnDeck,
} from '../../step-forms'
import type { LabwareDefByDefURI } from '../../labware-defs'

import styles from './DeckSetup.css'

Expand Down Expand Up @@ -102,13 +107,19 @@ const getModuleSlotDefs = (
)
}

const getSwapBlocked = (args: {
export const getSwapBlocked = (args: {
hoveredLabware: ?LabwareOnDeckType,
draggedLabware: ?LabwareOnDeckType,
modulesById: $PropertyType<InitialDeckSetup, 'modules'>,
customLabwareDefs: LabwareDefByDefURI,
}): boolean => {
const { hoveredLabware, draggedLabware, modulesById } = args

const {
hoveredLabware,
draggedLabware,
modulesById,
customLabwareDefs,
} = args
console.log('hovered ', hoveredLabware, modulesById)
if (!hoveredLabware || !draggedLabware) {
return false
}
Expand All @@ -118,11 +129,23 @@ const getSwapBlocked = (args: {
const destModuleType: ?ModuleRealType =
modulesById[hoveredLabware.slot]?.type || null

const draggedLabwareIsCustom = getLabwareIsCustom(
customLabwareDefs,
draggedLabware
)
const hoveredLabwareIsCustom = getLabwareIsCustom(
customLabwareDefs,
hoveredLabware
)

// dragging custom labware to module gives not compat error
const labwareSourceToDestBlocked = sourceModuleType
? !getLabwareIsCompatible(hoveredLabware.def, sourceModuleType)
? !getLabwareIsCompatible(hoveredLabware.def, sourceModuleType) &&
!hoveredLabwareIsCustom
: false
const labwareDestToSourceBlocked = destModuleType
? !getLabwareIsCompatible(draggedLabware.def, destModuleType)
? !getLabwareIsCompatible(draggedLabware.def, destModuleType) &&
!draggedLabwareIsCustom
: false

return labwareSourceToDestBlocked || labwareDestToSourceBlocked
Expand All @@ -147,11 +170,17 @@ const DeckSetupContents = (props: ContentsProps) => {
// whether swapping will be blocked due to labware<>module compat:
const [hoveredLabware, setHoveredLabware] = useState<?LabwareOnDeckType>(null)
const [draggedLabware, setDraggedLabware] = useState<?LabwareOnDeckType>(null)

const customLabwareDefs = useSelector(
labwareDefSelectors.getCustomLabwareDefsByURI
)
const swapBlocked = getSwapBlocked({
hoveredLabware,
draggedLabware,
modulesById: initialDeckSetup.modules,
customLabwareDefs,
})

const handleHoverEmptySlot = useCallback(() => setHoveredLabware(null), [])

const slotsBlockedBySpanning = getSlotsBlockedBySpanning(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,32 @@ import { connect } from 'react-redux'
import { DropTarget } from 'react-dnd'
import noop from 'lodash/noop'
import { i18n } from '../../../localization'
import { getLabwareIsCompatible } from '../../../utils/labwareModuleCompatibility'
import {
getLabwareIsCompatible,
getLabwareIsCustom,
} from '../../../utils/labwareModuleCompatibility'
import { BlockedSlot } from './BlockedSlot'
import {
openAddLabwareModal,
moveDeckItem,
} from '../../../labware-ingred/actions'
import { selectors as labwareDefSelectors } from '../../../labware-defs'
import { START_TERMINAL_ITEM_ID, type TerminalItemId } from '../../../steplist'
import { DND_TYPES } from './constants'

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

type DNDP = {|
isOver: boolean,
connectDropTarget: Node => mixed,
draggedDef: ?LabwareDefinition2,
connectDropTarget: Node => Node,
draggedItem: ?{ labwareOnDeck: LabwareOnDeck },
|}
type OP = {|
slot: {| ...DeckSlotDefinition, id: DeckSlot |}, // NOTE: Ian 2019-10-22 make slot `id` more restrictive when used in PD
Expand All @@ -39,26 +44,35 @@ type DP = {|
addLabware: (e: SyntheticEvent<*>) => mixed,
moveDeckItem: (DeckSlot, DeckSlot) => mixed,
|}
type Props = {| ...OP, ...DP, ...DNDP |}
type SP = {|
customLabwareDefs: LabwareDefByDefURI,
|}
type Props = {| ...OP, ...DP, ...DNDP, ...SP |}

const SlotControlsComponent = (props: Props) => {
export const SlotControlsComponent = (props: Props) => {
const {
slot,
addLabware,
selectedTerminalItemId,
isOver,
connectDropTarget,
moduleType,
draggedDef,
draggedItem,
customLabwareDefs,
} = props
if (selectedTerminalItemId !== START_TERMINAL_ITEM_ID) return null

const draggedDef = draggedItem?.labwareOnDeck?.def
const isCustomLabware = draggedItem
? getLabwareIsCustom(customLabwareDefs, draggedItem.labwareOnDeck)
: false

let slotBlocked: string | null = null
if (
isOver &&
moduleType != null &&
draggedDef != null &&
!getLabwareIsCompatible(draggedDef, moduleType)
(!getLabwareIsCompatible(draggedDef, moduleType) && !isCustomLabware)
) {
slotBlocked = 'Labware incompatible with this module'
}
Expand Down Expand Up @@ -98,6 +112,12 @@ const SlotControlsComponent = (props: Props) => {
)
}

const mapStateToProps = (state: BaseState): SP => {
return {
customLabwareDefs: labwareDefSelectors.getCustomLabwareDefsByURI(state),
}
}

const mapDispatchToProps = (dispatch: ThunkDispatch<*>, ownProps: OP): DP => ({
addLabware: () => dispatch(openAddLabwareModal({ slot: ownProps.slot.id })),
moveDeckItem: (sourceSlot, destSlot) =>
Expand All @@ -124,19 +144,24 @@ const slotTarget = {

if (moduleType != null && draggedDef != null) {
// this is a module slot, prevent drop if the dragged labware is not compatible
return getLabwareIsCompatible(draggedDef, moduleType)
const isCustomLabware = getLabwareIsCustom(
props.customLabwareDefs,
draggedItem.labwareOnDeck
)

return getLabwareIsCompatible(draggedDef, moduleType) || isCustomLabware
}
return true
},
}
const collectSlotTarget = (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
draggedDef: monitor.getItem()?.labwareOnDeck?.def || null,
draggedItem: monitor.getItem(),
})

export const SlotControls = connect<{| ...OP, ...DP |}, OP, _, DP, _, _>(
null,
export const SlotControls = connect<{| ...OP, ...DP, ...SP |}, OP, _, DP, _, _>(
mapStateToProps,
mapDispatchToProps
)(
DropTarget(DND_TYPES.LABWARE, slotTarget, collectSlotTarget)(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// @flow

import React from 'react'
import { shallow } from 'enzyme'
import fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json'
import { MAGNETIC_MODULE_TYPE } from '@opentrons/shared-data'
import * as labwareModuleCompatibility from '../../../../utils/labwareModuleCompatibility'
import { START_TERMINAL_ITEM_ID } from '../../../../steplist'
import { SlotControlsComponent } from '../SlotControls'
import { BlockedSlot } from '../BlockedSlot'

describe('SlotControlsComponent', () => {
let props, getLabwareIsCompatibleSpy
beforeEach(() => {
const slot = {
id: 'deckSlot1',
position: [1, 2, 3],
boundingBox: {
xDimension: 10,
yDimension: 20,
zDimension: 40,
},
displayName: 'slot 1',
compatibleModules: ['magdeck'],
}

const labwareOnDeck = {
labwareDefURI: 'fixture/fixture_96_plate',
id: 'plate123',
slot: '3',
def: fixture_96_plate,
}

props = {
slot,
addLabware: jest.fn(),
moveDeckItem: jest.fn(),
selectedTerminalItemId: START_TERMINAL_ITEM_ID,
isOver: true,
connectDropTarget: el => <svg>{el}</svg>,
moduleType: MAGNETIC_MODULE_TYPE,
draggedItem: {
labwareOnDeck,
},
customLabwareDefs: {},
}

getLabwareIsCompatibleSpy = jest.spyOn(
labwareModuleCompatibility,
'getLabwareIsCompatible'
)
})

afterEach(() => {
getLabwareIsCompatibleSpy.mockClear()
})

it('renders nothing when not start terminal item', () => {
props.selectedTerminalItemId = '__end__'

const wrapper = shallow(<SlotControlsComponent {...props} />)

expect(wrapper.get(0)).toBeNull()
})

it('gives a slot blocked warning when dragged noncustom and incompatible labware is over a module slot with labware', () => {
getLabwareIsCompatibleSpy.mockReturnValue(false)

const wrapper = shallow(<SlotControlsComponent {...props} />)
const blockedSlot = wrapper.find(BlockedSlot)

expect(blockedSlot.prop('message')).toBe(
'MODULE_INCOMPATIBLE_SINGLE_LABWARE'
)
})

it('displays place here when dragged compatible labware is hovered over slot with labware', () => {
getLabwareIsCompatibleSpy.mockReturnValue(true)

const wrapper = shallow(<SlotControlsComponent {...props} />)

expect(wrapper.render().text()).toContain('Place Here')
})

it('displays place here when dragged labware is custom and hovered over another labware on module slot', () => {
props.customLabwareDefs = {
'fixture/fixture_96_plate': fixture_96_plate,
}

const wrapper = shallow(<SlotControlsComponent {...props} />)

expect(wrapper.render().text()).toContain('Place Here')
})

it('displays add labware when slot is empty', () => {
props.isOver = false

const wrapper = shallow(<SlotControlsComponent {...props} />)

expect(wrapper.render().text()).toContain('Add Labware')
})
})
Loading

0 comments on commit 1c4e1b5

Please sign in to comment.