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): error handling in create file wizard #13804

Merged
merged 4 commits into from
Oct 19, 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
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',
jerader marked this conversation as resolved.
Show resolved Hide resolved
}
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')
})
})
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