From 82e04597ae9cfdab2ee3422b830b331378f3d700 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 22 May 2024 08:54:34 -0400 Subject: [PATCH] feat(app): "Cancel run" during Error Recovery (#15240) Closes EXEC-462 Adds the ability to cancel a run during Error Recovery. --- .../localization/en/error_recovery.json | 3 + .../ErrorRecoveryWizard.tsx | 13 ++- .../ErrorRecoveryFlows/RecoveryInProgress.tsx | 3 + .../RecoveryOptions/CancelRun.tsx | 76 +++++++++++++++ .../RecoveryOptions/ResumeRun.tsx | 2 +- .../__tests__/CancelRun.test.tsx | 93 +++++++++++++++++++ .../RecoveryOptions/index.ts | 1 + .../shared/RecoveryFooterButtons.tsx | 13 +-- .../__tests__/ErrorRecoveryFlows.test.tsx | 15 ++- .../__tests__/ErrorRecoveryWizard.test.tsx | 14 +++ .../__tests__/RecoveryInProgress.test.tsx | 14 +++ .../__tests__/useRecoveryCommands.test.ts | 22 ++++- .../organisms/ErrorRecoveryFlows/constants.ts | 15 ++- .../organisms/ErrorRecoveryFlows/index.tsx | 18 +++- app/src/organisms/ErrorRecoveryFlows/types.ts | 3 + .../ErrorRecoveryFlows/useRecoveryCommands.ts | 12 ++- 16 files changed, 292 insertions(+), 25 deletions(-) create mode 100644 app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx create mode 100644 app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index 6ba2520aebbd..1f634c3b3701 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -1,7 +1,9 @@ { + "are_you_sure_you_want_to_cancel": "Are you sure you want to cancel?", "are_you_sure_you_want_to_resume": "Are you sure you want to resume?", "before_you_begin": "Before you begin", "cancel_run": "Cancel run", + "canceling_run": "Canceling run", "confirm": "Confirm", "continue": "Continue", "general_error": "General error", @@ -13,6 +15,7 @@ "resume": "Resume", "run_paused": "Run paused", "run_will_resume": "The run will resume from the point at which the error occurred. Take any necessary actions to correct the problem first. If the step is completed successfully, the protocol continues.", + "if_tips_are_attached": "If tips are attached, you can choose to blow out any aspirated liquid and drop tips before the run is terminated.", "stand_back": "Stand back, robot is in motion", "stand_back_resuming": "Stand back, resuming current step", "stand_back_retrying": "Stand back, retrying current command", diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index 55fbf5047b95..422256345f37 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -11,9 +11,9 @@ import { } from '@opentrons/components' import { getIsOnDevice } from '../../redux/config' -import { getModalPortalEl } from '../../App/portal' +import { getTopPortalEl } from '../../App/portal' import { BeforeBeginning } from './BeforeBeginning' -import { SelectRecoveryOption, ResumeRun } from './RecoveryOptions' +import { SelectRecoveryOption, ResumeRun, CancelRun } from './RecoveryOptions' import { ErrorRecoveryHeader } from './ErrorRecoveryHeader' import { RecoveryInProgress } from './RecoveryInProgress' import { getErrorKind, useRouteUpdateActions } from './utils' @@ -80,7 +80,7 @@ function ErrorRecoveryComponent(props: RecoveryContentProps): JSX.Element { , - getModalPortalEl() + getTopPortalEl() ) } @@ -101,6 +101,10 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return } + const buildCancelRun = (): JSX.Element => { + return + } + switch (props.recoveryMap.route) { case RECOVERY_MAP.BEFORE_BEGINNING.ROUTE: return buildBeforeBeginning() @@ -108,9 +112,12 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return buildSelectRecoveryOption() case RECOVERY_MAP.RESUME.ROUTE: return buildResumeRun() + case RECOVERY_MAP.CANCEL_RUN.ROUTE: + return buildCancelRun() case RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE: case RECOVERY_MAP.ROBOT_RESUMING.ROUTE: case RECOVERY_MAP.ROBOT_RETRYING_COMMAND.ROUTE: + case RECOVERY_MAP.ROBOT_CANCELING.ROUTE: return buildRecoveryInProgress() default: return buildSelectRecoveryOption() diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx index 271233529332..94ad1cc64d1a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx @@ -10,6 +10,7 @@ export function RecoveryInProgress({ recoveryMap, }: RecoveryContentProps): JSX.Element { const { + ROBOT_CANCELING, ROBOT_IN_MOTION, ROBOT_RESUMING, ROBOT_RETRYING_COMMAND, @@ -19,6 +20,8 @@ export function RecoveryInProgress({ const buildDescription = (): RobotMovingRoute => { switch (route) { + case ROBOT_CANCELING.ROUTE: + return t('canceling_run') case ROBOT_IN_MOTION.ROUTE: return t('stand_back') case ROBOT_RESUMING.ROUTE: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx new file mode 100644 index 000000000000..91ff0f7985a1 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + COLORS, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + SPACING, + StyledText, +} from '@opentrons/components' + +import { RECOVERY_MAP } from '../constants' +import { RecoveryFooterButtons } from './shared' + +import type { RecoveryContentProps } from '../types' + +export function CancelRun({ + isOnDevice, + routeUpdateActions, + recoveryCommands, +}: RecoveryContentProps): JSX.Element | null { + const { ROBOT_CANCELING } = RECOVERY_MAP + const { t } = useTranslation('error_recovery') + + const { cancelRun } = recoveryCommands + const { goBackPrevStep, setRobotInMotion } = routeUpdateActions + + const primaryBtnOnClick = (): Promise => { + return setRobotInMotion(true, ROBOT_CANCELING.ROUTE).then(() => cancelRun()) + } + + if (isOnDevice) { + return ( + + + + + {t('are_you_sure_you_want_to_cancel')} + + + {t('if_tips_are_attached')} + + + + + ) + } else { + return null + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ResumeRun.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ResumeRun.tsx index d702db177f91..3fb935747e01 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ResumeRun.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ResumeRun.tsx @@ -28,7 +28,7 @@ export function ResumeRun({ const { goBackPrevStep, setRobotInMotion } = routeUpdateActions const primaryBtnOnClick = (): Promise => { - return setRobotInMotion(true, ROBOT_RETRYING_COMMAND.ROUTE) // Show the "retrying" motion screen while exiting ER. + return setRobotInMotion(true, ROBOT_RETRYING_COMMAND.ROUTE) .then(() => retryFailedCommand()) .then(() => resumeRun()) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx new file mode 100644 index 000000000000..24a8c0c29ed2 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx @@ -0,0 +1,93 @@ +import * as React from 'react' +import { vi, describe, it, expect, beforeEach } from 'vitest' +import { screen, fireEvent, waitFor } from '@testing-library/react' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { CancelRun } from '../CancelRun' +import { RECOVERY_MAP, ERROR_KINDS } from '../../constants' + +import type { Mock } from 'vitest' + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('RecoveryFooterButtons', () => { + const { CANCEL_RUN, ROBOT_CANCELING } = RECOVERY_MAP + let props: React.ComponentProps + let mockGoBackPrevStep: Mock + + beforeEach(() => { + mockGoBackPrevStep = vi.fn() + const mockRouteUpdateActions = { goBackPrevStep: mockGoBackPrevStep } as any + + props = { + isOnDevice: true, + recoveryCommands: {} as any, + failedCommand: {} as any, + errorKind: ERROR_KINDS.GENERAL_ERROR, + routeUpdateActions: mockRouteUpdateActions, + recoveryMap: { + route: CANCEL_RUN.ROUTE, + step: CANCEL_RUN.STEPS.CONFIRM_CANCEL, + }, + } + }) + + it('renders appropriate copy and click behavior', async () => { + render(props) + + screen.getByText('Are you sure you want to cancel?') + screen.queryByText( + 'If tips are attached, you can choose to blowout any aspirated liquid and drop tips before the run is terminated.' + ) + + const secondaryBtn = screen.getByRole('button', { name: 'Go back' }) + + fireEvent.click(secondaryBtn) + + expect(mockGoBackPrevStep).toHaveBeenCalled() + }) + + it('should call commands in the correct order for the primaryOnClick callback', async () => { + const setRobotInMotionMock = vi.fn(() => Promise.resolve()) + const cancelRunMock = vi.fn(() => Promise.resolve()) + + const mockRecoveryCommands = { + cancelRun: cancelRunMock, + } as any + + const mockRouteUpdateActions = { + setRobotInMotion: setRobotInMotionMock, + } as any + + render({ + ...props, + recoveryCommands: mockRecoveryCommands, + routeUpdateActions: mockRouteUpdateActions, + }) + + const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) + fireEvent.click(primaryBtn) + + await waitFor(() => { + expect(setRobotInMotionMock).toHaveBeenCalledTimes(1) + }) + await waitFor(() => { + expect(setRobotInMotionMock).toHaveBeenCalledWith( + true, + ROBOT_CANCELING.ROUTE + ) + }) + await waitFor(() => { + expect(cancelRunMock).toHaveBeenCalledTimes(1) + }) + + expect(setRobotInMotionMock.mock.invocationCallOrder[0]).toBeLessThan( + cancelRunMock.mock.invocationCallOrder[0] + ) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts index c39d9e883c65..12f638458efc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts @@ -1,2 +1,3 @@ export { SelectRecoveryOption } from './SelectRecoveryOption' export { ResumeRun } from './ResumeRun' +export { CancelRun } from './CancelRun' diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/shared/RecoveryFooterButtons.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/shared/RecoveryFooterButtons.tsx index 64264630c2d3..63e167fc3553 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/shared/RecoveryFooterButtons.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/shared/RecoveryFooterButtons.tsx @@ -4,16 +4,11 @@ import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, Flex, - JUSTIFY_CENTER, JUSTIFY_SPACE_BETWEEN, SPACING, } from '@opentrons/components' import { SmallButton } from '../../../../atoms/buttons' -import { - NON_SANCTIONED_RECOVERY_COLOR_STYLE_PRIMARY, - NON_SANCTIONED_RECOVERY_COLOR_STYLE_SECONDARY, -} from '../../constants' interface RecoveryOptionProps { isOnDevice: boolean @@ -39,20 +34,14 @@ export function RecoveryFooterButtons({ gridGap={SPACING.spacing8} > diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx index d44e3d4b3338..2c3e8848a564 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx @@ -5,6 +5,7 @@ import { screen, renderHook, act } from '@testing-library/react' import { RUN_STATUS_AWAITING_RECOVERY, RUN_STATUS_RUNNING, + RUN_STATUS_STOP_REQUESTED, } from '@opentrons/api-client' import { renderWithProviders } from '../../../__testing-utils__' @@ -33,7 +34,7 @@ describe('useErrorRecovery', () => { expect(result.current.isERActive).toBe(false) }) - it('should toggle the value of isEREnabled properly', () => { + it('should toggle the value of isEREnabled properly when the run status is valid', () => { const { result } = renderHook(() => useErrorRecoveryFlows('MOCK_ID', RUN_STATUS_AWAITING_RECOVERY) ) @@ -48,9 +49,19 @@ describe('useErrorRecovery', () => { }) expect(result.current.isERActive).toBe(false) + + const { result: resultStopRequested } = renderHook(() => + useErrorRecoveryFlows('MOCK_ID', RUN_STATUS_STOP_REQUESTED) + ) + + act(() => { + resultStopRequested.current.toggleER() + }) + + expect(resultStopRequested.current.isERActive).toBe(true) }) - it('should disable error recovery when runStatus is not "awaiting-recovery"', () => { + it('should disable error recovery when runStatus is not a valid ER run status', () => { const { result, rerender } = renderHook( (runStatus: RunStatus) => useErrorRecoveryFlows('MOCK_ID', runStatus), { diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx index 496fd676b9cf..e6f77dd4c80c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx @@ -31,6 +31,7 @@ describe('ErrorRecoveryContent', () => { OPTION_SELECTION, BEFORE_BEGINNING, RESUME, + ROBOT_CANCELING, ROBOT_RESUMING, ROBOT_IN_MOTION, ROBOT_RETRYING_COMMAND, @@ -92,6 +93,19 @@ describe('ErrorRecoveryContent', () => { screen.getByText('MOCK_RESUME_RUN') }) + it(`returns RecoveryInProgressModal when the route is ${ROBOT_CANCELING.ROUTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + route: ROBOT_CANCELING.ROUTE, + }, + } + render(props) + + screen.getByText('MOCK_IN_PROGRESS') + }) + it(`returns RecoveryInProgressModal when the route is ${ROBOT_IN_MOTION.ROUTE}`, () => { props = { ...props, diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx index a280a270970f..fc9523f4efb1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx @@ -15,6 +15,7 @@ const render = (props: React.ComponentProps) => { describe('RecoveryInProgress', () => { const { + ROBOT_CANCELING, ROBOT_IN_MOTION, ROBOT_RESUMING, ROBOT_RETRYING_COMMAND, @@ -66,4 +67,17 @@ describe('RecoveryInProgress', () => { screen.getByText('Stand back, retrying current command') }) + + it(`renders appropriate copy when the route is ${ROBOT_CANCELING.ROUTE}`, () => { + props = { + ...props, + recoveryMap: { + route: ROBOT_CANCELING.ROUTE, + step: ROBOT_CANCELING.STEPS.CANCELING, + }, + } + render(props) + + screen.getByText('Canceling run') + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/useRecoveryCommands.test.ts b/app/src/organisms/ErrorRecoveryFlows/__tests__/useRecoveryCommands.test.ts index 801b5785b7e4..26d654f9cef6 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/useRecoveryCommands.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/useRecoveryCommands.test.ts @@ -1,7 +1,10 @@ import { vi, it, describe, expect, beforeEach } from 'vitest' import { renderHook, act } from '@testing-library/react' -import { useResumeRunFromRecoveryMutation } from '@opentrons/react-api-client' +import { + useResumeRunFromRecoveryMutation, + useStopRunMutation, +} from '@opentrons/react-api-client' import { useChainRunCommands } from '../../../resources/runs' import { @@ -21,12 +24,16 @@ const mockRunId = '123' describe('useRecoveryCommands', () => { const mockResumeRunFromRecovery = vi.fn() + const mockStopRun = vi.fn() const mockChainRunCommands = vi.fn().mockResolvedValue([]) beforeEach(() => { vi.mocked(useResumeRunFromRecoveryMutation).mockReturnValue({ resumeRunFromRecovery: mockResumeRunFromRecovery, } as any) + vi.mocked(useStopRunMutation).mockReturnValue({ + stopRun: mockStopRun, + } as any) vi.mocked(useChainRunCommands).mockReturnValue({ chainRunCommands: mockChainRunCommands, } as any) @@ -86,6 +93,19 @@ describe('useRecoveryCommands', () => { expect(mockResumeRunFromRecovery).toHaveBeenCalledWith(mockRunId) }) + it('should call cancelRun with runId', () => { + const { result } = renderHook(() => + useRecoveryCommands({ + runId: mockRunId, + failedCommand: mockFailedCommand, + }) + ) + + result.current.cancelRun() + + expect(mockStopRun).toHaveBeenCalledWith(mockRunId) + }) + it('should call homePipetteZAxes with the appropriate command', async () => { const { result } = renderHook(() => useRecoveryCommands({ diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index e0da763d076c..bf428b2aa081 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -17,7 +17,10 @@ export const RECOVERY_MAP = { RECOVERY_DESCRIPTION: 'recovery-description', }, }, - CANCEL_RUN: { ROUTE: 'cancel-run', STEPS: {} }, + CANCEL_RUN: { + ROUTE: 'cancel-run', + STEPS: { CONFIRM_CANCEL: 'confirm-cancel' }, + }, DROP_TIP: { ROUTE: 'drop-tip', STEPS: {} }, IGNORE_AND_RESUME: { ROUTE: 'ignore-and-resume', STEPS: {} }, REFILL_AND_RESUME: { ROUTE: 'refill-and-resume', STEPS: {} }, @@ -25,6 +28,12 @@ export const RECOVERY_MAP = { ROUTE: 'resume', STEPS: { CONFIRM_RESUME: 'confirm-resume' }, }, + ROBOT_CANCELING: { + ROUTE: 'robot-cancel-run', + STEPS: { + CANCELING: 'canceling', + }, + }, ROBOT_IN_MOTION: { ROUTE: 'robot-in-motion', STEPS: { @@ -53,6 +62,7 @@ const { BEFORE_BEGINNING, OPTION_SELECTION, RESUME, + ROBOT_CANCELING, ROBOT_RESUMING, ROBOT_IN_MOTION, ROBOT_RETRYING_COMMAND, @@ -67,13 +77,14 @@ export const STEP_ORDER: StepOrder = { [BEFORE_BEGINNING.ROUTE]: [BEFORE_BEGINNING.STEPS.RECOVERY_DESCRIPTION], [OPTION_SELECTION.ROUTE]: [OPTION_SELECTION.STEPS.SELECT], [RESUME.ROUTE]: [RESUME.STEPS.CONFIRM_RESUME], + [ROBOT_CANCELING.ROUTE]: [ROBOT_CANCELING.STEPS.CANCELING], [ROBOT_IN_MOTION.ROUTE]: [ROBOT_IN_MOTION.STEPS.IN_MOTION], [ROBOT_RESUMING.ROUTE]: [ROBOT_RESUMING.STEPS.RESUMING], [ROBOT_RETRYING_COMMAND.ROUTE]: [ROBOT_RETRYING_COMMAND.STEPS.RETRYING], [DROP_TIP.ROUTE]: [], [REFILL_AND_RESUME.ROUTE]: [], [IGNORE_AND_RESUME.ROUTE]: [], - [CANCEL_RUN.ROUTE]: [], + [CANCEL_RUN.ROUTE]: [CANCEL_RUN.STEPS.CONFIRM_CANCEL], } export const INVALID = 'INVALID' as const diff --git a/app/src/organisms/ErrorRecoveryFlows/index.tsx b/app/src/organisms/ErrorRecoveryFlows/index.tsx index b7da1f4caaf6..2a41f2fca2f3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/index.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/index.tsx @@ -1,6 +1,9 @@ import * as React from 'react' -import { RUN_STATUS_AWAITING_RECOVERY } from '@opentrons/api-client' +import { + RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_STOP_REQUESTED, +} from '@opentrons/api-client' import { ErrorRecoveryWizard } from './ErrorRecoveryWizard' import { useCurrentlyFailedRunCommand } from './utils' @@ -8,6 +11,11 @@ import { useCurrentlyFailedRunCommand } from './utils' import type { RunStatus } from '@opentrons/api-client' import type { FailedCommand } from './types' +const VALID_ER_RUN_STATUSES: RunStatus[] = [ + RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_STOP_REQUESTED, +] + interface UseErrorRecoveryResult { isERActive: boolean failedCommand: FailedCommand | null @@ -25,9 +33,13 @@ export function useErrorRecoveryFlows( setIsERActive(!isERActive) } - // Because multiple ER flows may occur per run, disable ER when the status is not "awaiting-recovery." + // Because multiple ER flows may occur per run, disable ER when the status is not "awaiting-recovery" or a + // terminating run status in which we want to persist ER flows. React.useEffect(() => { - if (isERActive && runStatus !== RUN_STATUS_AWAITING_RECOVERY) { + const isValidRunStatus = + runStatus != null && VALID_ER_RUN_STATUSES.includes(runStatus) + + if (isERActive && !isValidRunStatus) { setIsERActive(false) } }, [isERActive, runStatus]) diff --git a/app/src/organisms/ErrorRecoveryFlows/types.ts b/app/src/organisms/ErrorRecoveryFlows/types.ts index 794b36e679e5..00da630fedb8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/types.ts +++ b/app/src/organisms/ErrorRecoveryFlows/types.ts @@ -10,6 +10,7 @@ export type RobotMovingRoute = | typeof RECOVERY_MAP['ROBOT_IN_MOTION']['ROUTE'] | typeof RECOVERY_MAP['ROBOT_RESUMING']['ROUTE'] | typeof RECOVERY_MAP['ROBOT_RETRYING_COMMAND']['ROUTE'] + | typeof RECOVERY_MAP['ROBOT_CANCELING']['ROUTE'] export type ErrorKind = keyof typeof ERROR_KINDS interface RecoveryMapDetails { @@ -27,6 +28,7 @@ type RecoveryStep< K extends keyof RecoveryMap > = RecoveryMap[K]['STEPS'][keyof RecoveryMap[K]['STEPS']] +type RobotCancellingRunStep = RecoveryStep<'ROBOT_CANCELING'> type RobotInMotionStep = RecoveryStep<'ROBOT_IN_MOTION'> type RobotResumingStep = RecoveryStep<'ROBOT_RESUMING'> type RobotRetryingCommandStep = RecoveryStep<'ROBOT_RETRYING_COMMAND'> @@ -49,6 +51,7 @@ export type RouteStep = | ResumeStep | OptionSelectionStep | RefillAndResumeStep + | RobotCancellingRunStep export interface IRecoveryMap { route: RecoveryRoute diff --git a/app/src/organisms/ErrorRecoveryFlows/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/useRecoveryCommands.ts index 78cab04f2d4d..59956e0f1fb1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/useRecoveryCommands.ts +++ b/app/src/organisms/ErrorRecoveryFlows/useRecoveryCommands.ts @@ -1,6 +1,9 @@ import * as React from 'react' -import { useResumeRunFromRecoveryMutation } from '@opentrons/react-api-client' +import { + useResumeRunFromRecoveryMutation, + useStopRunMutation, +} from '@opentrons/react-api-client' import { useChainRunCommands } from '../../resources/runs' @@ -14,6 +17,7 @@ interface UseRecoveryCommandsParams { } export interface UseRecoveryCommandsResult { resumeRun: () => void + cancelRun: () => void retryFailedCommand: () => Promise homePipetteZAxes: () => Promise } @@ -24,6 +28,7 @@ export function useRecoveryCommands({ }: UseRecoveryCommandsParams): UseRecoveryCommandsResult { const { chainRunCommands } = useChainRunCommands(runId, failedCommand?.id) const { resumeRunFromRecovery } = useResumeRunFromRecoveryMutation() + const { stopRun } = useStopRunMutation() const chainRunRecoveryCommands = React.useCallback( ( @@ -54,8 +59,13 @@ export function useRecoveryCommands({ resumeRunFromRecovery(runId) }, [runId, resumeRunFromRecovery]) + const cancelRun = React.useCallback((): void => { + stopRun(runId) + }, [runId]) + return { resumeRun, + cancelRun, retryFailedCommand, homePipetteZAxes, }