diff --git a/app/src/assets/localization/en/drop_tip_wizard.json b/app/src/assets/localization/en/drop_tip_wizard.json
index 66924d00210..fc3bf25dfdf 100644
--- a/app/src/assets/localization/en/drop_tip_wizard.json
+++ b/app/src/assets/localization/en/drop_tip_wizard.json
@@ -3,10 +3,12 @@
"begin_removal": "Begin removal",
"blowout_complete": "blowout complete",
"blowout_liquid": "Blow out liquid",
+ "cant_safely_drop_tips": "Can't safely drop tips",
"choose_blowout_location": "choose blowout location",
"choose_drop_tip_location": "choose tip-drop location",
"confirm_blowout_location": "Is the pipette positioned where the liquids should be blown out?",
"confirm_drop_tip_location": "Is the pipette positioned where the tips should be dropped?",
+ "confirm_removal_and_home": "Confirm removal and home",
"drop_tip_complete": "tip drop complete",
"drop_tip_failed": "The drop tip could not be completed. Contact customer support for assistance.",
"drop_tips": "drop tips",
@@ -21,6 +23,7 @@
"position_the_pipette": "position the pipette",
"remove_the_tips": "You may want to remove the tips from the {{mount}} Pipette before using it again in a protocol.",
"remove_the_tips_from_pipette": "You may want to remove the tips from the pipette before using it again in a protocol.",
+ "remove_the_tips_manually": "Remove the tips manually. Then home the gantry. Homing with tips attached could pull liquid into the pipette and damage it.",
"remove_tips": "Remove tips",
"select_blowout_slot": "You can blow out liquid into a labware or dispose of it.Select the slot where you want to blow out the liquid on the deck map to the right. Once confirmed, the gantry will move to the chosen slot.",
"select_blowout_slot_odd": "You can blow out liquid into a labware or dispose of it.
After the gantry moves to the chosen slot, use the jog controls to move the pipette to the exact position for blowing out.",
diff --git a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx
index 69a8d7de694..cd21cc3e1a4 100644
--- a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx
+++ b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx
@@ -24,30 +24,20 @@ import {
import { SmallButton, MediumButton } from '../../atoms/buttons'
import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal'
-// import { NeedHelpLink } from '../CalibrationPanels'
import blowoutVideo from '../../assets/videos/droptip-wizard/Blowout-Liquid.webm'
import droptipVideo from '../../assets/videos/droptip-wizard/Drop-tip.webm'
-// TODO: get help link article URL
-// const NEED_HELP_URL = ''
-
interface BeforeBeginningProps {
setShouldDispenseLiquid: (shouldDispenseLiquid: boolean) => void
createdMaintenanceRunId: string | null
isOnDevice: boolean
- isRobotMoving: boolean
}
export const BeforeBeginning = (
props: BeforeBeginningProps
): JSX.Element | null => {
- const {
- setShouldDispenseLiquid,
- createdMaintenanceRunId,
- isOnDevice,
- isRobotMoving,
- } = props
+ const { setShouldDispenseLiquid, createdMaintenanceRunId, isOnDevice } = props
const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared'])
const [flowType, setFlowType] = React.useState<
'liquid_and_tips' | 'only_tips' | null
@@ -57,16 +47,8 @@ export const BeforeBeginning = (
setShouldDispenseLiquid(flowType === 'liquid_and_tips')
}
- if (isRobotMoving || createdMaintenanceRunId == null) {
- return (
-
- )
+ if (createdMaintenanceRunId == null) {
+ return
}
if (isOnDevice) {
diff --git a/app/src/organisms/DropTipWizard/ChooseLocation.tsx b/app/src/organisms/DropTipWizard/ChooseLocation.tsx
index 8050c776698..7a86da67223 100644
--- a/app/src/organisms/DropTipWizard/ChooseLocation.tsx
+++ b/app/src/organisms/DropTipWizard/ChooseLocation.tsx
@@ -22,15 +22,13 @@ import {
import { getDeckDefFromRobotType } from '@opentrons/shared-data'
import { SmallButton } from '../../atoms/buttons'
-import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal'
-// import { NeedHelpLink } from '../CalibrationPanels'
import { TwoUpTileLayout } from '../LabwarePositionCheck/TwoUpTileLayout'
import type { CommandData } from '@opentrons/api-client'
import type { AddressableAreaName, RobotType } from '@opentrons/shared-data'
+import type { ErrorDetails } from './utils'
// TODO: get help link article URL
-// const NEED_HELP_URL = ''
interface ChooseLocationProps {
handleProceed: () => void
@@ -41,9 +39,8 @@ interface ChooseLocationProps {
moveToAddressableArea: (
addressableArea: AddressableAreaName
) => Promise
- isRobotMoving: boolean
isOnDevice: boolean
- setErrorMessage: (arg0: string) => void
+ setErrorDetails: (errorDetails: ErrorDetails) => void
}
export const ChooseLocation = (
@@ -56,9 +53,8 @@ export const ChooseLocation = (
body,
robotType,
moveToAddressableArea,
- isRobotMoving,
isOnDevice,
- setErrorMessage,
+ setErrorDetails,
} = props
const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared'])
const deckDef = getDeckDefFromRobotType(robotType)
@@ -74,14 +70,10 @@ export const ChooseLocation = (
if (deckSlot != null) {
moveToAddressableArea(deckSlot)
.then(() => handleProceed())
- .catch(e => setErrorMessage(`${e.message}`))
+ .catch(e => setErrorDetails({ message: `${e.message}` }))
}
}
- if (isRobotMoving) {
- return
- }
-
if (isOnDevice) {
return (
void
handleGoBack: () => void
- isRobotMoving: boolean
}
export function ExitConfirmation(props: ExitConfirmationProps): JSX.Element {
- const { handleGoBack, handleExit, isRobotMoving } = props
+ const { handleGoBack, handleExit } = props
const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared'])
const flowTitle = t('drop_tips')
const isOnDevice = useSelector(getIsOnDevice)
- if (isRobotMoving) {
- return
- }
-
return (
void
body: string
- isRobotMoving: boolean
currentStep: string
isOnDevice: boolean
}
@@ -161,7 +160,6 @@ export const JogToPosition = (
handleJog,
handleProceed,
body,
- isRobotMoving,
currentStep,
isOnDevice,
} = props
@@ -171,10 +169,10 @@ export const JogToPosition = (
setShowPositionConfirmation,
] = React.useState(false)
// Includes special case homing only present in this step.
- const [isRobotInMotion, setIsRobotInMotion] = React.useState(isRobotMoving)
+ const [isRobotInMotion, setIsRobotInMotion] = React.useState(false)
const onGoBack = (): void => {
- setIsRobotInMotion(() => true)
+ setIsRobotInMotion(true)
handleGoBack()
}
@@ -201,11 +199,6 @@ export const JogToPosition = (
)
}
- // Moving due to "Exit" or "Go back" click.
- if (isRobotInMotion) {
- return
- }
-
if (isOnDevice) {
return (
{
+ let props: UseDropTipErrorComponentsProps
+ let mockOnClose: Mock
+ let mockTranslation: Mock
+ let mockChainRunCommands: Mock
+
+ beforeEach(() => {
+ mockOnClose = vi.fn()
+ mockTranslation = vi.fn()
+ mockChainRunCommands = vi.fn()
+
+ props = {
+ maintenanceRunId: MOCK_MAINTENANCE_RUN_ID,
+ onClose: mockOnClose,
+ errorDetails: {
+ type: MOCK_ERROR_TYPE,
+ message: MOCK_ERROR_MESSAGE,
+ header: MOCK_ERROR_HEADER,
+ },
+ isOnDevice: true,
+ t: mockTranslation,
+ chainRunCommands: mockChainRunCommands,
+ }
+ })
+
+ it('should return the generic text and error message if there is are no special-cased error details', () => {
+ const result = useDropTipErrorComponents(props)
+ expect(result.button).toBeNull()
+ render(result.subHeader)
+ expect(mockTranslation).toHaveBeenCalledWith('drop_tip_failed')
+ screen.getByText(MOCK_ERROR_MESSAGE)
+ })
+
+ it('should return a generic message only if there are no error details', () => {
+ props.errorDetails = null
+ const result = useDropTipErrorComponents(props)
+ expect(result.button).toBeNull()
+ render(result.subHeader)
+ expect(mockTranslation).toHaveBeenCalledWith('drop_tip_failed')
+ expect(screen.queryByText(MOCK_ERROR_MESSAGE)).not.toBeInTheDocument()
+ })
+
+ it(`should return correct special components if error type is ${DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR}`, () => {
+ // @ts-expect-error errorDetails is in fact not null in the test.
+ props.errorDetails.type = DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR
+ const result = useDropTipErrorComponents(props)
+ expect(mockTranslation).toHaveBeenCalledWith('confirm_removal_and_home')
+
+ render(result.button)
+ const btn = screen.getByRole('button')
+ fireEvent.click(btn)
+ expect(mockOnClose).toHaveBeenCalled()
+ expect(mockChainRunCommands).toHaveBeenCalledWith(
+ MOCK_MAINTENANCE_RUN_ID,
+ [
+ {
+ commandType: 'home' as const,
+ params: {},
+ },
+ ],
+ true
+ )
+
+ render(result.subHeader)
+ screen.getByText(MOCK_ERROR_MESSAGE)
+ })
+})
+
+describe('useWizardExitHeader', () => {
+ let props: UseWizardExitHeaderProps
+ let mockHandleCleanUpAndClose: Mock
+ let mockConfirmExit: Mock
+
+ beforeEach(() => {
+ mockHandleCleanUpAndClose = vi.fn()
+ mockConfirmExit = vi.fn()
+
+ props = {
+ isFinalStep: true,
+ hasInitiatedExit: false,
+ errorDetails: null,
+ handleCleanUpAndClose: mockHandleCleanUpAndClose,
+ confirmExit: mockConfirmExit,
+ }
+ })
+
+ it('should appropriately return handleCleanUpAndClose', () => {
+ const handleExit = useWizardExitHeader(props)
+ expect(handleExit).toEqual(props.handleCleanUpAndClose)
+ })
+
+ it('should appropriately return confirmExit', () => {
+ props = { ...props, isFinalStep: false }
+ const handleExit = useWizardExitHeader(props)
+ expect(handleExit).toEqual(props.confirmExit)
+ })
+
+ it('should appropriately return handleCleanUpAndClose with homeOnError = false', () => {
+ const errorDetails = { message: 'Some error occurred' }
+ const modifiedProps = { ...props, errorDetails }
+ const handleExit = useWizardExitHeader(modifiedProps)
+ expect(mockHandleCleanUpAndClose.mock.calls.length).toBe(0)
+ handleExit()
+ expect(mockHandleCleanUpAndClose).toHaveBeenCalledWith(false)
+ })
+
+ it('should appropriately return a function that does nothing ', () => {
+ const modifiedProps = { ...props, hasInitiatedExit: true }
+ const handleExit = useWizardExitHeader(modifiedProps)
+ handleExit()
+ expect(mockHandleCleanUpAndClose.mock.calls.length).toBe(0)
+ expect(mockConfirmExit.mock.calls.length).toBe(0)
+ })
+})
diff --git a/app/src/organisms/DropTipWizard/constants.ts b/app/src/organisms/DropTipWizard/constants.ts
index 0390fd1870f..6d322f779ec 100644
--- a/app/src/organisms/DropTipWizard/constants.ts
+++ b/app/src/organisms/DropTipWizard/constants.ts
@@ -16,3 +16,7 @@ export const DROP_TIP_STEPS = [
POSITION_AND_DROP_TIP,
DROP_TIP_SUCCESS,
]
+
+export const DROP_TIP_SPECIAL_ERROR_TYPES = {
+ MUST_HOME_ERROR: 'MustHomeError',
+} as const
diff --git a/app/src/organisms/DropTipWizard/index.tsx b/app/src/organisms/DropTipWizard/index.tsx
index 3d7896663d4..ca668cd7013 100644
--- a/app/src/organisms/DropTipWizard/index.tsx
+++ b/app/src/organisms/DropTipWizard/index.tsx
@@ -11,6 +11,7 @@ import {
COLORS,
BORDERS,
StyledText,
+ JUSTIFY_FLEX_END,
} from '@opentrons/components'
import {
useCreateMaintenanceCommandMutation,
@@ -43,6 +44,12 @@ import { BeforeBeginning } from './BeforeBeginning'
import { ChooseLocation } from './ChooseLocation'
import { JogToPosition } from './JogToPosition'
import { Success } from './Success'
+import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal'
+import {
+ useHandleDropTipCommandErrors,
+ useDropTipErrorComponents,
+ useWizardExitHeader,
+} from './utils'
import type { PipetteData } from '@opentrons/api-client'
import type { CreateMaintenanceRunType } from '@opentrons/react-api-client'
@@ -54,6 +61,8 @@ import type {
} from '@opentrons/shared-data'
import type { Axis, Sign, StepSize } from '../../molecules/JogControls/types'
import type { Jog } from '../../molecules/JogControls'
+import type { ErrorDetails } from './utils'
+import type { DropTipWizardStep } from './types'
const RUN_REFETCH_INTERVAL_MS = 5000
const JOG_COMMAND_TIMEOUT_MS = 10000
@@ -110,7 +119,7 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element {
})
.catch(e => e)
},
- onError: error => setErrorMessage(error.message),
+ onError: error => setErrorDetails({ message: error.message }),
})
const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({
@@ -141,14 +150,16 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element {
])
const [isExiting, setIsExiting] = React.useState(false)
- const [errorMessage, setErrorMessage] = React.useState(null)
+ const [errorDetails, setErrorDetails] = React.useState(
+ null
+ )
const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation({
onSuccess: () => closeFlow(),
onError: () => closeFlow(),
})
- const handleCleanUpAndClose = (): void => {
+ const handleCleanUpAndClose = (homeOnExit: boolean = true): void => {
if (hasCleanedUpAndClosed.current) return
hasCleanedUpAndClosed.current = true
@@ -156,23 +167,23 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element {
if (maintenanceRunData?.data.id == null) {
closeFlow()
} else {
- chainRunCommands(
- maintenanceRunData?.data.id,
- [
- {
- commandType: 'home' as const,
- params: { axes: ['leftZ', 'rightZ', 'x', 'y'] },
- },
- ],
- true
+ ;(homeOnExit
+ ? chainRunCommands(
+ maintenanceRunData?.data.id,
+ [
+ {
+ commandType: 'home' as const,
+ params: { axes: ['leftZ', 'rightZ', 'x', 'y'] },
+ },
+ ],
+ true
+ )
+ : new Promise((resolve, reject) => resolve())
)
- .then(() => {
- deleteMaintenanceRun(maintenanceRunData?.data.id)
- })
.catch(error => {
console.error(error.message)
- deleteMaintenanceRun(maintenanceRunData?.data.id)
})
+ .finally(() => deleteMaintenanceRun(maintenanceRunData?.data.id))
}
}
@@ -188,8 +199,8 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element {
handleCleanUpAndClose={handleCleanUpAndClose}
chainRunCommands={chainRunCommands}
createRunCommand={createMaintenanceCommand}
- errorMessage={errorMessage}
- setErrorMessage={setErrorMessage}
+ errorDetails={errorDetails}
+ setErrorDetails={setErrorDetails}
isExiting={isExiting}
deckConfig={deckConfig}
/>
@@ -203,9 +214,9 @@ interface DropTipWizardProps {
createMaintenanceRun: CreateMaintenanceRunType
isRobotMoving: boolean
isExiting: boolean
- setErrorMessage: (message: string | null) => void
- errorMessage: string | null
- handleCleanUpAndClose: () => void
+ setErrorDetails: (errorDetails: ErrorDetails) => void
+ errorDetails: ErrorDetails | null
+ handleCleanUpAndClose: (homeOnError?: boolean) => void
chainRunCommands: ReturnType<
typeof useChainMaintenanceCommands
>['chainRunCommands']
@@ -227,20 +238,23 @@ export const DropTipWizardComponent = (
chainRunCommands,
isRobotMoving,
createRunCommand,
- setErrorMessage,
- errorMessage,
+ setErrorDetails,
+ errorDetails,
isExiting,
createdMaintenanceRunId,
instrumentModelSpecs,
deckConfig,
} = props
- const isOnDevice = useSelector(getIsOnDevice)
const { t, i18n } = useTranslation('drop_tip_wizard')
-
const [currentStepIndex, setCurrentStepIndex] = React.useState(0)
const [shouldDispenseLiquid, setShouldDispenseLiquid] = React.useState<
boolean | null
>(null)
+ const hasInitiatedExit = React.useRef(false)
+
+ const isOnDevice = useSelector(getIsOnDevice)
+ const setSpecificErrorDetails = useHandleDropTipCommandErrors(setErrorDetails)
+
const DropTipWizardSteps = getDropTipWizardSteps(shouldDispenseLiquid)
const currentStep =
shouldDispenseLiquid != null
@@ -248,11 +262,31 @@ export const DropTipWizardComponent = (
: null
const isFinalStep = currentStepIndex === DropTipWizardSteps.length - 1
+ const {
+ confirm: confirmExit,
+ showConfirmation: showConfirmExit,
+ cancel: cancelExit,
+ } = useConditionalConfirm(handleCleanUpAndClose, true)
+
+ const {
+ button: errorExitBtn,
+ subHeader: errorSubHeader,
+ } = useDropTipErrorComponents({
+ t,
+ errorDetails,
+ isOnDevice,
+ chainRunCommands,
+ maintenanceRunId: createdMaintenanceRunId,
+ onClose: handleCleanUpAndClose,
+ })
+
React.useEffect(() => {
if (createdMaintenanceRunId == null) {
- createMaintenanceRun({}).catch((e: Error) =>
- setErrorMessage(`Error creating maintenance run: ${e.message}`)
- )
+ createMaintenanceRun({}).catch((e: Error) => {
+ setSpecificErrorDetails({
+ message: `Error creating maintenance run: ${e.message}`,
+ })
+ })
}
}, [])
@@ -280,18 +314,14 @@ export const DropTipWizardComponent = (
},
waitUntilComplete: true,
timeout: JOG_COMMAND_TIMEOUT_MS,
- }).catch((e: Error) =>
- setErrorMessage(`Error issuing jog command: ${e.message}`)
- )
+ }).catch((e: Error) => {
+ setSpecificErrorDetails({
+ message: `Error issuing jog command: ${e.message}`,
+ })
+ })
}
}
- const {
- confirm: confirmExit,
- showConfirmation: showConfirmExit,
- cancel: cancelExit,
- } = useConditionalConfirm(handleCleanUpAndClose, true)
-
const moveToAddressableArea = (
addressableArea: AddressableAreaName
): Promise => {
@@ -326,189 +356,228 @@ export const DropTipWizardComponent = (
).then(commandData => {
const error = commandData[0].data.error
if (error != null) {
- setErrorMessage(`error moving to position: ${error.detail}`)
+ setSpecificErrorDetails({
+ runCommandError: error,
+ message: `Error moving to position: ${error.detail}`,
+ })
}
return null
})
} else {
- setErrorMessage(`error moving to position: invalid addressable area.`)
+ setSpecificErrorDetails({
+ message: `Error moving to position: invalid addressable area.`,
+ })
return Promise.resolve(null)
}
}
- let modalContent: JSX.Element = UNASSIGNED STEP
- if (showConfirmExit) {
- modalContent = (
- {
- hasInitiatedExit.current = true
- confirmExit()
- }}
- isRobotMoving={isRobotMoving}
- />
- )
- } else if (errorMessage != null) {
- modalContent = (
-
- {t('drop_tip_failed')}
- {errorMessage}
- >
- }
- />
- )
- } else if (shouldDispenseLiquid == null) {
- modalContent = (
-
- )
- } else if (
- currentStep === CHOOSE_BLOWOUT_LOCATION ||
- currentStep === CHOOSE_DROP_TIP_LOCATION
- ) {
- let bodyTextKey
- if (currentStep === CHOOSE_BLOWOUT_LOCATION) {
- bodyTextKey = isOnDevice
- ? 'select_blowout_slot_odd'
- : 'select_blowout_slot'
+ const modalContent = buildModalContent()
+
+ function buildModalContent(): JSX.Element {
+ if (isRobotMoving) {
+ return buildRobotInMotion()
+ } else if (showConfirmExit) {
+ return buildShowExitConfirmation()
+ } else if (errorDetails != null) {
+ return buildErrorScreen()
+ } else if (shouldDispenseLiquid == null) {
+ return buildBeforeBeginning()
+ } else if (
+ currentStep === CHOOSE_BLOWOUT_LOCATION ||
+ currentStep === CHOOSE_DROP_TIP_LOCATION
+ ) {
+ return buildChooseLocation()
+ } else if (
+ currentStep === POSITION_AND_BLOWOUT ||
+ currentStep === POSITION_AND_DROP_TIP
+ ) {
+ return buildJogToPosition()
+ } else if (
+ currentStep === BLOWOUT_SUCCESS ||
+ currentStep === DROP_TIP_SUCCESS
+ ) {
+ return buildSuccess()
} else {
- bodyTextKey = isOnDevice
- ? 'select_drop_tip_slot_odd'
- : 'select_drop_tip_slot'
+ return UNASSIGNED STEP
}
- modalContent = (
- {
- setCurrentStepIndex(0)
- setShouldDispenseLiquid(null)
- }}
- title={
- currentStep === CHOOSE_BLOWOUT_LOCATION
- ? i18n.format(t('choose_blowout_location'), 'capitalize')
- : i18n.format(t('choose_drop_tip_location'), 'capitalize')
- }
- body={
- }}
- />
- }
- moveToAddressableArea={moveToAddressableArea}
- isRobotMoving={isRobotMoving}
- isOnDevice={isOnDevice}
- setErrorMessage={setErrorMessage}
- />
- )
- } else if (
- currentStep === POSITION_AND_BLOWOUT ||
- currentStep === POSITION_AND_DROP_TIP
- ) {
- modalContent = (
- {
- if (createdMaintenanceRunId != null) {
- chainRunCommands(
- createdMaintenanceRunId,
- [
- currentStep === POSITION_AND_BLOWOUT
- ? {
- commandType: 'blowOutInPlace',
- params: {
- pipetteId: MANAGED_PIPETTE_ID,
- flowRate:
- instrumentModelSpecs.defaultBlowOutFlowRate.value,
+
+ function buildRobotInMotion(): JSX.Element {
+ return
+ }
+
+ function buildShowExitConfirmation(): JSX.Element {
+ return (
+ {
+ hasInitiatedExit.current = true
+ confirmExit()
+ }}
+ />
+ )
+ }
+
+ function buildErrorScreen(): JSX.Element {
+ return (
+
+ {errorExitBtn}
+
+ )
+ }
+
+ function buildBeforeBeginning(): JSX.Element {
+ return (
+
+ )
+ }
+
+ function buildChooseLocation(): JSX.Element {
+ let bodyTextKey: string
+ if (currentStep === CHOOSE_BLOWOUT_LOCATION) {
+ bodyTextKey = isOnDevice
+ ? 'select_blowout_slot_odd'
+ : 'select_blowout_slot'
+ } else {
+ bodyTextKey = isOnDevice
+ ? 'select_drop_tip_slot_odd'
+ : 'select_drop_tip_slot'
+ }
+ return (
+ {
+ setCurrentStepIndex(0)
+ setShouldDispenseLiquid(null)
+ }}
+ title={
+ currentStep === CHOOSE_BLOWOUT_LOCATION
+ ? i18n.format(t('choose_blowout_location'), 'capitalize')
+ : i18n.format(t('choose_drop_tip_location'), 'capitalize')
+ }
+ body={
+ }}
+ />
+ }
+ moveToAddressableArea={moveToAddressableArea}
+ isOnDevice={isOnDevice}
+ setErrorDetails={setSpecificErrorDetails}
+ />
+ )
+ }
+
+ function buildJogToPosition(): JSX.Element {
+ return (
+ {
+ if (createdMaintenanceRunId != null) {
+ chainRunCommands(
+ createdMaintenanceRunId,
+ [
+ currentStep === POSITION_AND_BLOWOUT
+ ? {
+ commandType: 'blowOutInPlace',
+ params: {
+ pipetteId: MANAGED_PIPETTE_ID,
+ flowRate:
+ instrumentModelSpecs.defaultBlowOutFlowRate.value,
+ },
+ }
+ : {
+ commandType: 'dropTipInPlace',
+ params: { pipetteId: MANAGED_PIPETTE_ID },
},
- }
- : {
- commandType: 'dropTipInPlace',
- params: { pipetteId: MANAGED_PIPETTE_ID },
- },
- ],
- true
- )
- .then(commandData => {
- const error = commandData[0].data.error
- if (error != null) {
- setErrorMessage(`error moving to position: ${error.detail}`)
- } else proceed()
- })
- .catch(e =>
- setErrorMessage(
- `Error issuing ${
- currentStep === POSITION_AND_BLOWOUT
- ? 'blowout'
- : 'drop tip'
- } command: ${e.message}`
- )
+ ],
+ true
)
+ .then(commandData => {
+ const error = commandData[0].data.error
+ if (error != null) {
+ setSpecificErrorDetails({
+ runCommandError: error,
+ message: `Error moving to position: ${error.detail}`,
+ })
+ } else {
+ proceed()
+ }
+ })
+ .catch(e =>
+ setSpecificErrorDetails({
+ message: `Error issuing ${
+ currentStep === POSITION_AND_BLOWOUT
+ ? 'blowout'
+ : 'drop tip'
+ } command: ${e.message}`,
+ })
+ )
+ }
+ }}
+ handleGoBack={goBack}
+ body={
+ currentStep === POSITION_AND_BLOWOUT
+ ? t('position_and_blowout')
+ : t('position_and_drop_tip')
}
- }}
- isRobotMoving={isRobotMoving}
- handleGoBack={goBack}
- body={
- currentStep === POSITION_AND_BLOWOUT
- ? t('position_and_blowout')
- : t('position_and_drop_tip')
- }
- currentStep={currentStep}
- isOnDevice={isOnDevice}
- />
- )
- } else if (
- currentStep === BLOWOUT_SUCCESS ||
- currentStep === DROP_TIP_SUCCESS
- ) {
- modalContent = (
-
- )
+ currentStep={currentStep as DropTipWizardStep}
+ isOnDevice={isOnDevice}
+ />
+ )
+ }
+
+ function buildSuccess(): JSX.Element {
+ return (
+
+ )
+ }
}
- const hasInitiatedExit = React.useRef(false)
- let handleExit: () => void = () => null
- if (!hasInitiatedExit.current) handleExit = confirmExit
- else if (errorMessage != null) handleExit = handleCleanUpAndClose
+ const wizardHeaderOnExit = useWizardExitHeader({
+ isFinalStep,
+ hasInitiatedExit: hasInitiatedExit.current,
+ errorDetails,
+ confirmExit,
+ handleCleanUpAndClose,
+ })
const wizardHeader = (
)
diff --git a/app/src/organisms/DropTipWizard/utils.tsx b/app/src/organisms/DropTipWizard/utils.tsx
new file mode 100644
index 00000000000..d0a38fc768b
--- /dev/null
+++ b/app/src/organisms/DropTipWizard/utils.tsx
@@ -0,0 +1,185 @@
+import * as React from 'react'
+import { useTranslation } from 'react-i18next'
+
+import { AlertPrimaryButton, SPACING } from '@opentrons/components'
+
+import { DROP_TIP_SPECIAL_ERROR_TYPES } from './constants'
+import { SmallButton } from '../../atoms/buttons'
+
+import type { RunCommandError } from '@opentrons/api-client'
+import type { useChainMaintenanceCommands } from '../../resources/runs'
+
+export interface ErrorDetails {
+ message: string
+ header?: string
+ type?: string
+}
+
+interface HandleDropTipCommandErrorsCbProps {
+ runCommandError?: RunCommandError
+ message?: string
+ header?: string
+ type?: RunCommandError['errorType']
+}
+
+/**
+ * @description Wraps the error state setter, updating the setter if the error should be special-cased.
+ */
+export function useHandleDropTipCommandErrors(
+ setErrorDetails: (errorDetails: ErrorDetails) => void
+): (cbProps: HandleDropTipCommandErrorsCbProps) => void {
+ const { t } = useTranslation('drop_tip_wizard')
+
+ return ({
+ runCommandError,
+ message,
+ header,
+ type,
+ }: HandleDropTipCommandErrorsCbProps) => {
+ if (
+ runCommandError?.errorType ===
+ DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR
+ ) {
+ const headerText = t('cant_safely_drop_tips')
+ const messageText = t('remove_the_tips_manually')
+
+ setErrorDetails({
+ header: headerText,
+ message: messageText,
+ type: DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR,
+ })
+ } else {
+ const messageText = message ?? ''
+ setErrorDetails({ header, message: messageText, type })
+ }
+ }
+}
+
+interface DropTipErrorComponents {
+ button: JSX.Element | null
+ subHeader: JSX.Element
+}
+
+export interface UseDropTipErrorComponentsProps {
+ isOnDevice: boolean
+ t: (translationString: string) => string
+ maintenanceRunId: string | null
+ onClose: () => void
+ errorDetails: ErrorDetails | null
+ chainRunCommands: ReturnType<
+ typeof useChainMaintenanceCommands
+ >['chainRunCommands']
+}
+
+/**
+ * @description Returns special-cased components given error details.
+ */
+export function useDropTipErrorComponents({
+ t,
+ maintenanceRunId,
+ onClose,
+ errorDetails,
+ isOnDevice,
+ chainRunCommands,
+}: UseDropTipErrorComponentsProps): DropTipErrorComponents {
+ return errorDetails?.type === DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR
+ ? buildHandleMustHome()
+ : buildGenericError()
+
+ function buildGenericError(): DropTipErrorComponents {
+ return {
+ button: null,
+ subHeader: (
+ <>
+ {t('drop_tip_failed')}
+
+ {errorDetails?.message}
+ >
+ ),
+ }
+ }
+
+ function buildHandleMustHome(): DropTipErrorComponents {
+ const handleOnClick = (): void => {
+ if (maintenanceRunId !== null) {
+ void chainRunCommands(
+ maintenanceRunId,
+ [
+ {
+ commandType: 'home' as const,
+ params: {},
+ },
+ ],
+ true
+ )
+ onClose()
+ }
+ }
+
+ return {
+ button: isOnDevice ? (
+
+ ) : (
+
+ {t('confirm_removal_and_home')}
+
+ ),
+ subHeader: <>{errorDetails?.message}>,
+ }
+ }
+}
+
+export interface UseWizardExitHeaderProps {
+ isFinalStep: boolean
+ hasInitiatedExit: boolean
+ errorDetails: ErrorDetails | null
+ handleCleanUpAndClose: (homeOnError?: boolean) => void
+ confirmExit: (homeOnError?: boolean) => void
+}
+
+/**
+ * @description Determines the appropriate onClick for the wizard exit button, ensuring the exit logic can occur at
+ * most one time.
+ */
+export function useWizardExitHeader({
+ isFinalStep,
+ hasInitiatedExit,
+ errorDetails,
+ handleCleanUpAndClose,
+ confirmExit,
+}: UseWizardExitHeaderProps): () => void {
+ return buildHandleExit()
+
+ function buildHandleExit(): () => void {
+ if (!hasInitiatedExit) {
+ if (errorDetails != null) {
+ // When an error occurs, do not home when exiting the flow via the wizard header.
+ return buildNoHomeCleanUpAndClose()
+ } else if (isFinalStep) {
+ return buildHandleCleanUpAndClose()
+ } else {
+ return buildConfirmExit()
+ }
+ } else {
+ return buildGenericCase()
+ }
+ }
+
+ function buildGenericCase(): () => void {
+ return () => null
+ }
+ function buildNoHomeCleanUpAndClose(): () => void {
+ return () => handleCleanUpAndClose(false)
+ }
+ function buildHandleCleanUpAndClose(): () => void {
+ return handleCleanUpAndClose
+ }
+ function buildConfirmExit(): () => void {
+ return confirmExit
+ }
+}