Skip to content

Commit

Permalink
feat(protocol-designer): error handling in create file wizard (#13804)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerader authored Oct 19, 2023
1 parent 4ccb28c commit 07af61e
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 90 deletions.
46 changes: 26 additions & 20 deletions protocol-designer/src/components/DeckSetup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ import {
RobotType,
FLEX_ROBOT_TYPE,
Cutout,
TRASH_BIN_LOAD_NAME,
STAGING_AREA_LOAD_NAME,
WASTE_CHUTE_LOAD_NAME,
} from '@opentrons/shared-data'
import {
FLEX_TRASH_DEF_URI,
Expand Down Expand Up @@ -107,7 +110,6 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
robotType,
trashSlot,
} = props

// NOTE: handling module<>labware compat when moving labware to empty module
// is handled by SlotControls.
// But when swapping labware when at least one is on a module, we need to be aware
Expand Down Expand Up @@ -494,15 +496,15 @@ export const DeckSetup = (): JSX.Element => {
{
fixtureId: trash?.id,
fixtureLocation: trash?.slot as Cutout,
loadName: 'trashBin',
loadName: TRASH_BIN_LOAD_NAME,
},
]
const wasteChuteFixtures = Object.values(
activeDeckSetup.additionalEquipmentOnDeck
).filter(aE => aE.name === 'wasteChute')
).filter(aE => aE.name === WASTE_CHUTE_LOAD_NAME)
const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values(
activeDeckSetup.additionalEquipmentOnDeck
).filter(aE => aE.name === 'stagingArea')
).filter(aE => aE.name === STAGING_AREA_LOAD_NAME)
const locations = Object.values(
activeDeckSetup.additionalEquipmentOnDeck
).map(aE => aE.location)
Expand Down Expand Up @@ -545,22 +547,26 @@ export const DeckSetup = (): JSX.Element => {
fixtureBaseColor={lightFill}
/>
))}
{trashBinFixtures.map(fixture => (
<React.Fragment key={fixture.fixtureId}>
<SingleSlotFixture
cutoutLocation={fixture.fixtureLocation}
deckDefinition={deckDef}
slotClipColor={COLORS.transparent}
fixtureBaseColor={lightFill}
/>
<FlexTrash
robotType={robotType}
trashIconColor={lightFill}
trashLocation={fixture.fixtureLocation as TrashLocation}
backgroundColor={darkFill}
/>
</React.Fragment>
))}
{trash != null
? trashBinFixtures.map(fixture => (
<React.Fragment key={fixture.fixtureId}>
<SingleSlotFixture
cutoutLocation={fixture.fixtureLocation}
deckDefinition={deckDef}
slotClipColor={COLORS.transparent}
fixtureBaseColor={lightFill}
/>
<FlexTrash
robotType={robotType}
trashIconColor={lightFill}
trashLocation={
fixture.fixtureLocation as TrashLocation
}
backgroundColor={darkFill}
/>
</React.Fragment>
))
: null}
{wasteChuteFixtures.map(fixture => (
<WasteChuteFixture
key={fixture.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
import {
HEATERSHAKER_MODULE_TYPE,
MAGNETIC_MODULE_TYPE,
ModuleType,
TEMPERATURE_MODULE_TYPE,
getPipetteNameSpecs,
PipetteName,
Expand All @@ -31,38 +30,36 @@ import {
getModuleType,
FLEX_ROBOT_TYPE,
} from '@opentrons/shared-data'
import {
FormModulesByType,
getIsCrashablePipetteSelected,
} from '../../../step-forms'
import { getIsCrashablePipetteSelected } from '../../../step-forms'
import gripperImage from '../../../images/flex_gripper.png'
import wasteChuteImage from '../../../images/waste_chute.png'
import { i18n } from '../../../localization'
import { selectors as featureFlagSelectors } from '../../../feature-flags'
import {
CrashInfoBox,
ModuleDiagram,
isModuleWithCollisionIssue,
} from '../../modules'
import { CrashInfoBox, ModuleDiagram } from '../../modules'
import { ModuleFields } from '../FilePipettesModal/ModuleFields'
import { GoBack } from './GoBack'
import {
getCrashableModuleSelected,
getLastCheckedEquipment,
getTrashBinOptionDisabled,
} from './utils'
import { EquipmentOption } from './EquipmentOption'
import { HandleEnter } from './HandleEnter'

import type { AdditionalEquipment, WizardTileProps } from './types'

const getCrashableModuleSelected = (
modules: FormModulesByType,
moduleType: ModuleType
): boolean => {
const formModule = modules[moduleType]
const crashableModuleOnDeck =
formModule?.onDeck && formModule?.model != null
? isModuleWithCollisionIssue(formModule.model)
: false

return crashableModuleOnDeck
export const DEFAULT_SLOT_MAP: { [moduleModel in ModuleModel]?: string } = {
[THERMOCYCLER_MODULE_V2]: 'B1',
[HEATERSHAKER_MODULE_V1]: 'D1',
[MAGNETIC_BLOCK_V1]: 'D2',
[TEMPERATURE_MODULE_V2]: 'C1',
}
export const FLEX_SUPPORTED_MODULE_MODELS: ModuleModel[] = [
THERMOCYCLER_MODULE_V2,
HEATERSHAKER_MODULE_V1,
MAGNETIC_BLOCK_V1,
TEMPERATURE_MODULE_V2,
]

export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element {
const {
Expand Down Expand Up @@ -200,35 +197,14 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element {
</HandleEnter>
)
}

const FLEX_SUPPORTED_MODULE_MODELS: ModuleModel[] = [
THERMOCYCLER_MODULE_V2,
HEATERSHAKER_MODULE_V1,
MAGNETIC_BLOCK_V1,
TEMPERATURE_MODULE_V2,
]
const DEFAULT_SLOT_MAP: { [moduleModel in ModuleModel]?: string } = {
[THERMOCYCLER_MODULE_V2]: 'B1',
[HEATERSHAKER_MODULE_V1]: 'D1',
[MAGNETIC_BLOCK_V1]: 'D2',
[TEMPERATURE_MODULE_V2]: 'C1',
}

interface FlexModuleFieldsProps extends WizardTileProps {
enableDeckModification: boolean
}
function FlexModuleFields(props: FlexModuleFieldsProps): JSX.Element {
const { values, setFieldValue, enableDeckModification } = props

const isFlex = values.fields.robotType === FLEX_ROBOT_TYPE
const allStagingAreasInUse =
values.additionalEquipment.filter(equipment =>
equipment.includes('stagingArea')
).length === 4
const allModulesInSideSlotsOnDeck =
values.modulesByType.heaterShakerModuleType.onDeck &&
values.modulesByType.thermocyclerModuleType.onDeck &&
values.modulesByType.temperatureModuleType.onDeck
const trashBinDisabled = getTrashBinOptionDisabled(values)

const handleSetEquipmentOption = (equipment: AdditionalEquipment): void => {
if (values.additionalEquipment.includes(equipment)) {
Expand All @@ -244,17 +220,26 @@ function FlexModuleFields(props: FlexModuleFieldsProps): JSX.Element {
}
}

React.useEffect(() => {
if (trashBinDisabled) {
setFieldValue(
'additionalEquipment',
without(values.additionalEquipment, 'trashBin')
)
}
}, [trashBinDisabled, setFieldValue])

return (
<Flex flexWrap={WRAP} gridGap={SPACING.spacing4} alignSelf={ALIGN_CENTER}>
{FLEX_SUPPORTED_MODULE_MODELS.map(moduleModel => {
const moduleType = getModuleType(moduleModel)
return (
<EquipmentOption
// TODO(jr, 10/10/23): add disabled option here for if the deck is full
key={moduleModel}
isSelected={values.modulesByType[moduleType].onDeck}
image={<ModuleDiagram type={moduleType} model={moduleModel} />}
text={getModuleDisplayName(moduleModel)}
disabled={getLastCheckedEquipment(values) === moduleType}
onClick={() => {
if (values.modulesByType[moduleType].onDeck) {
setFieldValue(`modulesByType.${moduleType}.onDeck`, false)
Expand Down Expand Up @@ -311,7 +296,7 @@ function FlexModuleFields(props: FlexModuleFieldsProps): JSX.Element {
}
text="Trash Bin"
showCheckbox
disabled={allStagingAreasInUse && allModulesInSideSlotsOnDeck}
disabled={trashBinDisabled}
/>
</>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ describe('CreateFileWizard', () => {
next.click()
getByText('Step 6 / 7')
// select a staging area
getByText('Staging areas')
getByText('Staging area slots')
next = getByRole('button', { name: 'Next' })
next.click()
getByText('Step 7 / 7')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('StagingAreaTile', () => {
it('renders header and deck configurator', () => {
props.values.fields.robotType = FLEX_ROBOT_TYPE
const { getByText } = render(props)
getByText('Staging areas')
getByText('Staging area slots')
getByText('mock deck configurator')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
FLEX_ROBOT_TYPE,
TEMPERATURE_MODULE_TYPE,
} from '@opentrons/shared-data'
import {
FLEX_TRASH_DEFAULT_SLOT,
getLastCheckedEquipment,
getTrashSlot,
} from '../utils'
import type {
FormModulesByType,
FormPipettesByMount,
} from '../../../../step-forms'
import type { FormState } from '../types'

let MOCK_FORM_STATE = {
fields: {
name: 'mockName',
description: 'mockDescription',
organizationOrAuthor: 'mockOrganizationOrAuthor',
robotType: FLEX_ROBOT_TYPE,
},
pipettesByMount: {
left: { pipetteName: 'mockPipetteName', tiprackDefURI: 'mocktip' },
right: { pipetteName: null, tiprackDefURI: null },
} as FormPipettesByMount,
modulesByType: {
heaterShakerModuleType: { onDeck: false, model: null, slot: 'D1' },
magneticBlockType: { onDeck: false, model: null, slot: 'D2' },
temperatureModuleType: { onDeck: false, model: null, slot: 'C1' },
thermocyclerModuleType: { onDeck: false, model: null, slot: 'B1' },
} as FormModulesByType,
additionalEquipment: [],
} as FormState

describe('getLastCheckedEquipment', () => {
it('should return null when there is no trash bin', () => {
const result = getLastCheckedEquipment(MOCK_FORM_STATE)
expect(result).toBe(null)
})
it('should return null if not all the modules or staging areas are selected', () => {
MOCK_FORM_STATE = {
...MOCK_FORM_STATE,
additionalEquipment: ['trashBin'],
modulesByType: {
...MOCK_FORM_STATE.modulesByType,
temperatureModuleType: { onDeck: true, model: null, slot: 'C1' },
},
}
const result = getLastCheckedEquipment(MOCK_FORM_STATE)
expect(result).toBe(null)
})
it('should return temperature module if other modules and staging areas are selected', () => {
MOCK_FORM_STATE = {
...MOCK_FORM_STATE,
additionalEquipment: [
'trashBin',
'stagingArea_A3',
'stagingArea_B3',
'stagingArea_C3',
'stagingArea_D3',
],
modulesByType: {
...MOCK_FORM_STATE.modulesByType,
heaterShakerModuleType: { onDeck: true, model: null, slot: 'D1' },
thermocyclerModuleType: { onDeck: true, model: null, slot: 'B1' },
},
}
const result = getLastCheckedEquipment(MOCK_FORM_STATE)
expect(result).toBe(TEMPERATURE_MODULE_TYPE)
})
})

describe('getTrashSlot', () => {
it('should return the default slot A3 when there is no staging area in that slot', () => {
MOCK_FORM_STATE = {
...MOCK_FORM_STATE,
additionalEquipment: ['trashBin'],
}
const result = getTrashSlot(MOCK_FORM_STATE)
expect(result).toBe(FLEX_TRASH_DEFAULT_SLOT)
})
it('should return B3 when there is a staging area in slot A3', () => {
MOCK_FORM_STATE = {
...MOCK_FORM_STATE,
additionalEquipment: ['stagingArea_A3'],
}
const result = getTrashSlot(MOCK_FORM_STATE)
expect(result).toBe('B3')
})
})
32 changes: 13 additions & 19 deletions protocol-designer/src/components/modals/CreateFileWizard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import uniq from 'lodash/uniq'
import { Formik, FormikProps } from 'formik'
import * as Yup from 'yup'
import { ModalShell } from '@opentrons/components'
import { OT_2_TRASH_DEF_URI } from '@opentrons/step-generation'
import {
ModuleType,
ModuleModel,
Expand Down Expand Up @@ -60,11 +61,9 @@ import { FirstPipetteTipsTile, SecondPipetteTipsTile } from './PipetteTipsTile'
import { ModulesAndOtherTile } from './ModulesAndOtherTile'
import { WizardHeader } from './WizardHeader'
import { StagingAreaTile } from './StagingAreaTile'
import { getTrashSlot } from './utils'

import {
NormalizedPipette,
OT_2_TRASH_DEF_URI,
} from '@opentrons/step-generation'
import type { NormalizedPipette } from '@opentrons/step-generation'
import type { FormState } from './types'

type WizardStep =
Expand Down Expand Up @@ -201,28 +200,21 @@ export function CreateFileWizard(): JSX.Element | null {

// add trash
if (
enableDeckModification &&
values.additionalEquipment.includes('trashBin')
(enableDeckModification &&
values.additionalEquipment.includes('trashBin')) ||
!enableDeckModification
) {
// defaulting trash to appropriate locations
dispatch(
labwareIngredActions.createContainer({
labwareDefURI: FLEX_TRASH_DEF_URI,
slot: 'A3',
})
)
}
if (
!enableDeckModification ||
(enableDeckModification && values.fields.robotType === OT2_ROBOT_TYPE)
) {
dispatch(
labwareIngredActions.createContainer({
labwareDefURI:
values.fields.robotType === FLEX_ROBOT_TYPE
? FLEX_TRASH_DEF_URI
: OT_2_TRASH_DEF_URI,
slot: values.fields.robotType === FLEX_ROBOT_TYPE ? 'A3' : '12',
slot:
values.fields.robotType === FLEX_ROBOT_TYPE
? getTrashSlot(values)
: '12',
})
)
}
Expand Down Expand Up @@ -347,7 +339,9 @@ const initialFormState: FormState = {
slot: SPAN7_8_10_11_SLOT,
},
},
additionalEquipment: [],
// defaulting to selecting trashBin already to avoid user having to
// click to add a trash bin/waste chute. Delete once we support returnTip()
additionalEquipment: ['trashBin'],
}

const pipetteValidationShape = Yup.object().shape({
Expand Down
Loading

0 comments on commit 07af61e

Please sign in to comment.