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, components, step-generation): update modules section to accommodate gripper #12955

Merged
merged 13 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
18 changes: 15 additions & 3 deletions components/src/slotmap/SlotMap.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react'
import cx from 'classnames'
import { RobotType } from '@opentrons/shared-data'
import { Icon } from '../icons'
import styles from './styles.css'

Expand All @@ -13,28 +14,39 @@ export interface SlotMapProps {
collisionSlots?: string[]
/** Optional error styling */
isError?: boolean
robotType?: RobotType
}

const SLOT_MAP_SLOTS = [
const OT2_SLOT_MAP_SLOTS = [
['10', '11'],
['7', '8', '9'],
['4', '5', '6'],
['1', '2', '3'],
]

const FLEX_SLOT_MAP_SLOTS = [
['A1', 'A2', 'A3'],
['B1', 'B2', 'B3'],
['C1', 'C2', 'C3'],
['D1', 'D2', 'D3'],
]

const slotWidth = 33
const slotHeight = 23
const iconSize = 20
const numRows = 4
const numCols = 3

export function SlotMap(props: SlotMapProps): JSX.Element {
const { collisionSlots, occupiedSlots, isError } = props
const { collisionSlots, occupiedSlots, isError, robotType } = props
const slots =
robotType === 'OT-3 Standard' ? FLEX_SLOT_MAP_SLOTS : OT2_SLOT_MAP_SLOTS

return (
<svg
viewBox={`-1,-1,${slotWidth * numCols + 2}, ${slotHeight * numRows + 2}`}
>
{SLOT_MAP_SLOTS.flatMap((row, rowIndex) =>
{slots.flatMap((row, rowIndex) =>
row.map((slot, colIndex) => {
const isCollisionSlot =
collisionSlots && collisionSlots.includes(slot)
Expand Down
22 changes: 20 additions & 2 deletions components/src/slotmap/__tests__/SlotMap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { SlotMap } from '../SlotMap'
import { Icon } from '../../icons'

describe('SlotMap', () => {
it('component renders 11 slots', () => {
it('component renders 11 slots for ot-2', () => {
const wrapper = shallow(<SlotMap occupiedSlots={['1']} />)

expect(wrapper.find('rect')).toHaveLength(11)
})

it('component renders crash info icon when collision slots present', () => {
it('component renders crash info icon when collision slots present for ot-2', () => {
const wrapper = shallow(
<SlotMap occupiedSlots={['1']} collisionSlots={['4']} />
)
Expand All @@ -29,4 +29,22 @@ describe('SlotMap', () => {
expect(wrapperWithError.find('.slot_occupied')).toHaveLength(1)
expect(wrapperWithError.find('.slot_occupied.slot_error')).toHaveLength(1)
})

it('should render 12 slots for flex', () => {
const wrapper = shallow(
<SlotMap occupiedSlots={['D1']} robotType="OT-3 Standard" />
)
expect(wrapper.find('rect')).toHaveLength(12)
})

it('component renders crash info icon when collision slots present for flex', () => {
const wrapper = shallow(
<SlotMap
occupiedSlots={['D1']}
collisionSlots={['D2']}
robotType="OT-3 Standard"
/>
)
expect(wrapper.find(Icon)).toHaveLength(1)
})
})
50 changes: 24 additions & 26 deletions protocol-designer/src/components/DeckSetup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -432,33 +432,31 @@ export const DeckSetup = (): JSX.Element => {
})

return (
<React.Fragment>
<div className={styles.deck_row}>
{drilledDown && <BrowseLabwareModal />}
<div ref={wrapperRef} className={styles.deck_wrapper}>
<RobotWorkSpace
deckLayerBlocklist={DECK_LAYER_BLOCKLIST}
deckDef={deckDef}
viewBox={robotType === OT2_ROBOT_TYPE ? OT2_VIEWBOX : FLEX_VIEWBOX}
width="100%"
height="100%"
>
{({ deckSlotsById, getRobotCoordsFromDOMCoords }) => (
<DeckSetupContents
activeDeckSetup={activeDeckSetup}
selectedTerminalItemId={selectedTerminalItemId}
{...{
deckDef,
deckSlotsById,
getRobotCoordsFromDOMCoords,
showGen1MultichannelCollisionWarnings,
}}
/>
)}
</RobotWorkSpace>
</div>
<div className={styles.deck_row}>
{drilledDown && <BrowseLabwareModal />}
<div ref={wrapperRef} className={styles.deck_wrapper}>
<RobotWorkSpace
deckLayerBlocklist={DECK_LAYER_BLOCKLIST}
deckDef={deckDef}
viewBox={robotType === OT2_ROBOT_TYPE ? OT2_VIEWBOX : FLEX_VIEWBOX}
width="100%"
height="100%"
>
{({ deckSlotsById, getRobotCoordsFromDOMCoords }) => (
<DeckSetupContents
activeDeckSetup={activeDeckSetup}
selectedTerminalItemId={selectedTerminalItemId}
{...{
deckDef,
deckSlotsById,
getRobotCoordsFromDOMCoords,
showGen1MultichannelCollisionWarnings,
}}
/>
)}
</RobotWorkSpace>
</div>
</React.Fragment>
</div>
)
}

Expand Down
59 changes: 53 additions & 6 deletions protocol-designer/src/components/FileSidebar/FileSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
SidePanel,
} from '@opentrons/components'
import { i18n } from '../../localization'
import { useBlockingHint } from '../Hints/useBlockingHint'
import { KnowledgeBaseLink } from '../KnowledgeBaseLink'
import { resetScrollElements } from '../../ui/steps/utils'
import { Portal } from '../portals/MainPageModalPortal'
import { useBlockingHint } from '../Hints/useBlockingHint'
import { KnowledgeBaseLink } from '../KnowledgeBaseLink'
import { getUnusedEntities } from './utils'
import modalStyles from '../modals/modal.css'
import styles from './FileSidebar.css'
Expand All @@ -22,7 +22,11 @@ import type {
ModuleOnDeck,
PipetteOnDeck,
} from '../../step-forms'
import type { CreateCommand, ProtocolFile } from '@opentrons/shared-data'
import type {
CreateCommand,
ProtocolFile,
RobotType,
} from '@opentrons/shared-data'

export interface Props {
loadFile: (event: React.ChangeEvent<HTMLInputElement>) => unknown
Expand All @@ -33,6 +37,15 @@ export interface Props {
pipettesOnDeck: InitialDeckSetup['pipettes']
modulesOnDeck: InitialDeckSetup['modules']
savedStepForms: SavedStepFormState
robotType: RobotType
additionalEquipment: AdditionalEquipment
}

export interface AdditionalEquipment {
[additionalEquipmentId: string]: {
name: 'gripper'
id: string
}
}

interface WarningContent {
Expand All @@ -44,6 +57,7 @@ interface MissingContent {
noCommands: boolean
pipettesWithoutStep: PipetteOnDeck[]
modulesWithoutStep: ModuleOnDeck[]
gripperWithoutStep: boolean
}

const LOAD_COMMANDS: Array<CreateCommand['commandType']> = [
Expand All @@ -57,6 +71,7 @@ function getWarningContent({
noCommands,
pipettesWithoutStep,
modulesWithoutStep,
gripperWithoutStep,
}: MissingContent): WarningContent | null {
if (noCommands) {
return {
Expand All @@ -73,6 +88,18 @@ function getWarningContent({
}
}

if (gripperWithoutStep) {
return {
content: (
<>
<p>{i18n.t('alert.export_warnings.unused_gripper.body1')}</p>
<p>{i18n.t('alert.export_warnings.unused_gripper.body2')}</p>
</>
),
heading: i18n.t('alert.export_warnings.unused_gripper.heading'),
}
}

const pipettesDetails = pipettesWithoutStep
.map(pipette => `${pipette.mount} ${pipette.spec.displayName}`)
.join(' and ')
Expand Down Expand Up @@ -159,11 +186,16 @@ export function FileSidebar(props: Props): JSX.Element {
modulesOnDeck,
pipettesOnDeck,
savedStepForms,
robotType,
additionalEquipment,
} = props
const [
showExportWarningModal,
setShowExportWarningModal,
] = React.useState<boolean>(false)
const isGripperAttached = Object.values(additionalEquipment).some(
equipment => equipment?.name === 'gripper'
)

const [showBlockingHint, setShowBlockingHint] = React.useState<boolean>(false)

Expand All @@ -174,27 +206,42 @@ export function FileSidebar(props: Props): JSX.Element {
command => !LOAD_COMMANDS.includes(command.commandType)
) ?? []

const gripperInUse =
fileData?.commands.find(
command =>
command.commandType === 'moveLabware' &&
command.params.strategy === 'usingGripper'
) != null

const noCommands = fileData ? nonLoadCommands.length === 0 : true
const pipettesWithoutStep = getUnusedEntities(
pipettesOnDeck,
savedStepForms,
'pipette'
'pipette',
robotType
)
const modulesWithoutStep = getUnusedEntities(
modulesOnDeck,
savedStepForms,
'moduleId'
'moduleId',
robotType
)

const gripperWithoutStep = isGripperAttached && !gripperInUse

const hasWarning =
noCommands || modulesWithoutStep.length || pipettesWithoutStep.length
noCommands ||
modulesWithoutStep.length ||
pipettesWithoutStep.length ||
gripperWithoutStep

const warning =
hasWarning &&
getWarningContent({
noCommands,
pipettesWithoutStep,
modulesWithoutStep,
gripperWithoutStep,
})

const getExportHintContent = (): {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import {
fixtureP300Single,
} from '@opentrons/shared-data/pipette/fixtures/name'
import fixture_tiprack_10_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json'
import { FileSidebar, v6WarningContent } from '../FileSidebar'
import { useBlockingHint } from '../../Hints/useBlockingHint'
import { FileSidebar, v6WarningContent } from '../FileSidebar'

jest.mock('../../Hints/useBlockingHint')
jest.mock('../../../file-data/selectors')
jest.mock('../../../step-forms/selectors')

const mockUseBlockingHint = useBlockingHint as jest.MockedFunction<
typeof useBlockingHint
Expand Down Expand Up @@ -50,6 +52,8 @@ describe('FileSidebar', () => {
pipettesOnDeck: {},
modulesOnDeck: {},
savedStepForms: {},
robotType: 'OT-2 Standard',
additionalEquipment: {},
}

commands = [
Expand Down Expand Up @@ -94,7 +98,6 @@ describe('FileSidebar', () => {
afterEach(() => {
jest.resetAllMocks()
})

it('create new button creates new protocol', () => {
const wrapper = shallow(<FileSidebar {...props} />)
const createButton = wrapper.find(OutlineButton).at(0)
Expand Down Expand Up @@ -154,6 +157,25 @@ describe('FileSidebar', () => {
)
})

it('warning modal is shown when export is clicked with unused gripper', () => {
const gripperId = 'gripperId'
props.modulesOnDeck = modulesOnDeck
props.savedStepForms = savedStepForms
// @ts-expect-error(sa, 2021-6-22): props.fileData might be null
props.fileData.commands = commands
props.additionalEquipment = {
[gripperId]: { name: 'gripper', id: gripperId },
}

const wrapper = shallow(<FileSidebar {...props} />)
const downloadButton = wrapper.find(DeprecatedPrimaryButton).at(0)
downloadButton.simulate('click')
const alertModal = wrapper.find(AlertModal)

expect(alertModal).toHaveLength(1)
expect(alertModal.prop('heading')).toEqual('Unused gripper')
})

it('warning modal is shown when export is clicked with unused module', () => {
props.modulesOnDeck = modulesOnDeck
props.savedStepForms = savedStepForms
Expand Down
Loading