diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json
index 605c2b55527..dd483a3daac 100644
--- a/app/src/assets/localization/en/error_recovery.json
+++ b/app/src/assets/localization/en/error_recovery.json
@@ -11,6 +11,7 @@
"change_location": "Change location",
"change_tip_pickup_location": "Change tip pick-up location",
"choose_a_recovery_action": "Choose a recovery action",
+ "close_door_to_resume": "Close robot door to resume",
"close_the_robot_door": "Close the robot door, and then resume the recovery action.",
"confirm": "Confirm",
"continue": "Continue",
@@ -24,6 +25,7 @@
"first_take_any_necessary_actions": "First, take any necessary actions to prepare the robot to retry the failed step.Then, close the robot door before proceeding.",
"go_back": "Go back",
"homing_pipette_dangerous": "Homing the {{mount}} pipette with liquid in the tips may damage it. You must remove all tips before using the pipette again.",
+ "if_issue_persists": " If the issue persists, cancel the run and make the necessary changes to the protocol",
"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.",
"ignore_all_errors_of_this_type": "Ignore all errors of this type",
"ignore_error_and_skip": "Ignore error and skip to next step",
@@ -36,7 +38,6 @@
"next_try_another_action": "Next, you can try another recovery action or cancel the run.",
"no_liquid_detected": "No liquid detected",
"overpressure_is_usually_caused": "Overpressure is usually caused by a tip contacting labware, a clog, or moving viscous liquid too quickly",
- "if_issue_persists": " If the issue persists, cancel the run and make the necessary changes to the protocol",
"pick_up_tips": "Pick up tips",
"pipette_overpressure": "Pipette overpressure",
"preserve_aspirated_liquid": "First, do you need to preserve aspirated liquid?",
diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
index d1809209764..b808844c62c 100644
--- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
@@ -453,8 +453,8 @@ export function ProtocolRunHeader({
{/* Note: This banner is for before running a protocol */}
{isDoorOpen &&
runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR &&
- runStatus !== RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR &&
runStatus != null &&
+ !RECOVERY_STATUSES.includes(runStatus) &&
CANCELLABLE_STATUSES.includes(runStatus) ? (
{t('shared:close_robot_door')}
diff --git a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx
similarity index 62%
rename from app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx
rename to app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx
index 30b916f7b11..041cc85cadb 100644
--- a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx
@@ -3,24 +3,34 @@ import styled, { css } from 'styled-components'
import { useTranslation } from 'react-i18next'
import {
- Flex,
- Icon,
- JUSTIFY_CENTER,
ALIGN_CENTER,
- SPACING,
COLORS,
DIRECTION_COLUMN,
- POSITION_ABSOLUTE,
- TYPOGRAPHY,
- OVERFLOW_WRAP_BREAK_WORD,
DISPLAY_FLEX,
+ Flex,
+ Icon,
+ JUSTIFY_CENTER,
JUSTIFY_SPACE_BETWEEN,
- TEXT_ALIGN_CENTER,
- StyledText,
+ OVERFLOW_WRAP_BREAK_WORD,
+ POSITION_ABSOLUTE,
PrimaryButton,
SecondaryButton,
+ SPACING,
+ StyledText,
+ TEXT_ALIGN_CENTER,
+ TYPOGRAPHY,
} from '@opentrons/components'
+import {
+ RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
+ RUN_STATUS_AWAITING_RECOVERY_PAUSED,
+} from '@opentrons/api-client'
+import type {
+ ERUtilsResults,
+ UseRecoveryAnalyticsResult,
+ UseRecoveryTakeoverResult,
+ useRetainedFailedCommandBySource,
+} from './hooks'
import { useErrorName } from './hooks'
import { getErrorKind } from './utils'
import { LargeButton } from '../../atoms/buttons'
@@ -30,17 +40,13 @@ import {
RECOVERY_MAP,
} from './constants'
import { RecoveryInterventionModal, StepInfo } from './shared'
+import { useToaster } from '../ToasterOven'
+import { WARNING_TOAST } from '../../atoms/Toast'
import type { RobotType } from '@opentrons/shared-data'
import type { ErrorRecoveryFlowsProps } from '.'
-import type {
- ERUtilsResults,
- UseRecoveryAnalyticsResult,
- UseRecoveryTakeoverResult,
- useRetainedFailedCommandBySource,
-} from './hooks'
-export function useRunPausedSplash(
+export function useRecoverySplash(
isOnDevice: boolean,
showERWizard: boolean
): boolean {
@@ -53,18 +59,17 @@ export function useRunPausedSplash(
}
}
-type RunPausedSplashProps = ERUtilsResults & {
- isOnDevice: boolean
- failedCommand: ReturnType
- protocolAnalysis: ErrorRecoveryFlowsProps['protocolAnalysis']
- robotType: RobotType
- robotName: string
- toggleERWizAsActiveUser: UseRecoveryTakeoverResult['toggleERWizAsActiveUser']
- analytics: UseRecoveryAnalyticsResult
-}
-export function RunPausedSplash(
- props: RunPausedSplashProps
-): JSX.Element | null {
+type RecoverySplashProps = ErrorRecoveryFlowsProps &
+ ERUtilsResults & {
+ isOnDevice: boolean
+ isWizardActive: boolean
+ failedCommand: ReturnType
+ robotType: RobotType
+ robotName: string
+ toggleERWizAsActiveUser: UseRecoveryTakeoverResult['toggleERWizAsActiveUser']
+ analytics: UseRecoveryAnalyticsResult
+ }
+export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null {
const {
isOnDevice,
toggleERWizAsActiveUser,
@@ -72,10 +77,14 @@ export function RunPausedSplash(
failedCommand,
analytics,
robotName,
+ runStatus,
+ recoveryActionMutationUtils,
+ isWizardActive,
} = props
const { t } = useTranslation('error_recovery')
const errorKind = getErrorKind(failedCommand?.byRunRecord ?? null)
const title = useErrorName(errorKind)
+ const { makeToast } = useToaster()
const { proceedToRouteAndStep } = routeUpdateActions
const { reportErrorEvent } = analytics
@@ -88,18 +97,55 @@ export function RunPausedSplash(
)
}
+ // Resume recovery when the run when the door is closed.
+ // The CTA/flow for handling a door open event within the ER wizard is different, and because this splash always renders
+ // behind the wizard, we want to ensure we only implicitly resume recovery when only viewing the splash.
+ React.useEffect(() => {
+ if (runStatus === RUN_STATUS_AWAITING_RECOVERY_PAUSED && !isWizardActive) {
+ recoveryActionMutationUtils.resumeRecovery()
+ }
+ }, [runStatus, isWizardActive])
+ const buildDoorOpenAlert = (): void => {
+ makeToast(t('close_door_to_resume') as string, WARNING_TOAST)
+ }
+
+ const handleConditionalClick = (onClick: () => void): void => {
+ switch (runStatus) {
+ case RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR:
+ buildDoorOpenAlert()
+ break
+ default:
+ onClick()
+ break
+ }
+ }
// Do not launch error recovery, but do utilize the wizard's cancel route.
- const onCancelClick = (): Promise => {
- return toggleERWizAsActiveUser(true, false).then(() => {
- reportErrorEvent(failedCommand?.byRunRecord ?? null, 'cancel-run')
- void proceedToRouteAndStep(RECOVERY_MAP.CANCEL_RUN.ROUTE)
- })
+ const onCancelClick = (): void => {
+ const onClick = (): void => {
+ void toggleERWizAsActiveUser(true, false).then(() => {
+ reportErrorEvent(failedCommand?.byRunRecord ?? null, 'cancel-run')
+ void proceedToRouteAndStep(RECOVERY_MAP.CANCEL_RUN.ROUTE)
+ })
+ }
+ handleConditionalClick(onClick)
}
- const onLaunchERClick = (): Promise => {
- return toggleERWizAsActiveUser(true, true).then(() => {
- reportErrorEvent(failedCommand?.byRunRecord ?? null, 'launch-recovery')
- })
+ const onLaunchERClick = (): void => {
+ const onClick = (): void => {
+ void toggleERWizAsActiveUser(true, true).then(() => {
+ reportErrorEvent(failedCommand?.byRunRecord ?? null, 'launch-recovery')
+ })
+ }
+ handleConditionalClick(onClick)
+ }
+
+ const isDisabled = (): boolean => {
+ switch (runStatus) {
+ case RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR:
+ return true
+ default:
+ return false
+ }
}
// TODO(jh 05-22-24): The hardcoded Z-indexing is non-ideal but must be done to keep the splash page above
@@ -149,14 +195,18 @@ export function RunPausedSplash(
@@ -194,12 +244,20 @@ export function RunPausedSplash(
-
+
{t('cancel_run')}
{t('launch_recovery_mode')}
@@ -234,6 +292,30 @@ const SHARED_BUTTON_STYLE_ODD = css`
width: 29rem;
height: 13.5rem;
`
+const BTN_STYLE_DISABLED_ODD = css`
+ ${SHARED_BUTTON_STYLE_ODD}
+
+ background-color: ${COLORS.grey35};
+ color: ${COLORS.grey50};
+ border: none;
+ box-shadow: none;
+
+ #btn-icon: {
+ color: ${COLORS.grey50};
+ }
+
+ &:active,
+ &:focus,
+ &:hover {
+ background-color: ${COLORS.grey35};
+ color: ${COLORS.grey50};
+ }
+ &:active,
+ &:focus,
+ &:hover #btn-icon {
+ color: ${COLORS.grey50};
+ }
+`
const PRIMARY_BTN_STYLES_DESKTOP = css`
background-color: ${COLORS.red50};
@@ -245,3 +327,17 @@ const PRIMARY_BTN_STYLES_DESKTOP = css`
background-color: ${COLORS.red55};
}
`
+const BTN_STYLES_DISABLED_DESKTOP = css`
+ background-color: ${COLORS.grey30};
+ color: ${COLORS.grey40};
+ border: none;
+ box-shadow: none;
+
+ &:active,
+ &:focus,
+ &:hover {
+ background-color: ${COLORS.grey30};
+ color: ${COLORS.grey40};
+ }
+ cursor: default;
+`
diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx
index 8ed839887ef..22a0e34109b 100644
--- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx
@@ -22,7 +22,7 @@ import {
} from '../hooks'
import { getIsOnDevice } from '../../../redux/config'
import { useERWizard, ErrorRecoveryWizard } from '../ErrorRecoveryWizard'
-import { useRunPausedSplash, RunPausedSplash } from '../RunPausedSplash'
+import { useRecoverySplash, RecoverySplash } from '../RecoverySplash'
import type { RunStatus } from '@opentrons/api-client'
@@ -30,7 +30,7 @@ vi.mock('../ErrorRecoveryWizard')
vi.mock('../hooks')
vi.mock('../useRecoveryCommands')
vi.mock('../../../redux/config')
-vi.mock('../RunPausedSplash')
+vi.mock('../RecoverySplash')
vi.mock('@opentrons/react-api-client')
vi.mock('react-redux', async () => {
const actual = await vi.importActual('react-redux')
@@ -143,15 +143,13 @@ describe('ErrorRecoveryFlows', () => {
protocolAnalysis: {} as any,
}
vi.mocked(ErrorRecoveryWizard).mockReturnValue(MOCK WIZARD
)
- vi.mocked(RunPausedSplash).mockReturnValue(
- MOCK RUN PAUSED SPLASH
- )
+ vi.mocked(RecoverySplash).mockReturnValue(MOCK RUN PAUSED SPLASH
)
vi.mocked(useERWizard).mockReturnValue({
hasLaunchedRecovery: true,
toggleERWizard: () => Promise.resolve(),
showERWizard: true,
})
- vi.mocked(useRunPausedSplash).mockReturnValue(true)
+ vi.mocked(useRecoverySplash).mockReturnValue(true)
vi.mocked(useERUtils).mockReturnValue({ routeUpdateActions: {} } as any)
vi.mocked(useShowDoorInfo).mockReturnValue(false)
vi.mocked(useRecoveryAnalytics).mockReturnValue({
@@ -202,7 +200,7 @@ describe('ErrorRecoveryFlows', () => {
})
it('does not render the splash when showSplash is false', () => {
- vi.mocked(useRunPausedSplash).mockReturnValue(false)
+ vi.mocked(useRecoverySplash).mockReturnValue(false)
render(props)
expect(screen.queryByText('MOCK RUN PAUSED SPLASH')).not.toBeInTheDocument()
})
diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx
similarity index 74%
rename from app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx
rename to app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx
index 0764b6c865e..c09a35ff5fc 100644
--- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx
@@ -3,20 +3,29 @@ import { MemoryRouter } from 'react-router-dom'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, screen, waitFor, renderHook } from '@testing-library/react'
import { createStore } from 'redux'
+import { QueryClient, QueryClientProvider } from 'react-query'
+import { Provider } from 'react-redux'
+
+import {
+ RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
+ RUN_STATUS_AWAITING_RECOVERY,
+ RUN_STATUS_AWAITING_RECOVERY_PAUSED,
+} from '@opentrons/api-client'
import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'
import { mockRecoveryContentProps } from '../__fixtures__'
import { getIsOnDevice } from '../../../redux/config'
-import { useRunPausedSplash, RunPausedSplash } from '../RunPausedSplash'
+import { useRecoverySplash, RecoverySplash } from '../RecoverySplash'
import { StepInfo } from '../shared'
+import { useToaster } from '../../ToasterOven'
+import { clickButtonLabeled } from './util'
import type { Store } from 'redux'
-import { QueryClient, QueryClientProvider } from 'react-query'
-import { Provider } from 'react-redux'
vi.mock('../../../redux/config')
vi.mock('../shared')
+vi.mock('../../ToasterOven')
const store: Store = createStore(vi.fn(), {})
@@ -45,7 +54,7 @@ describe('useRunPausedSplash', () => {
TEST_CASES.forEach(({ isOnDevice, showERWizard, expected }) => {
it(`returns ${expected} when isOnDevice is ${isOnDevice} and showERWizard is ${showERWizard}`, () => {
const { result } = renderHook(
- () => useRunPausedSplash(isOnDevice, showERWizard),
+ () => useRecoverySplash(isOnDevice, showERWizard),
{
wrapper,
}
@@ -56,10 +65,10 @@ describe('useRunPausedSplash', () => {
})
})
-const render = (props: React.ComponentProps) => {
+const render = (props: React.ComponentProps) => {
return renderWithProviders(
-
+
,
{
i18nInstance: i18n,
@@ -67,13 +76,15 @@ const render = (props: React.ComponentProps) => {
)
}
-describe('RunPausedSplash', () => {
- let props: React.ComponentProps
+describe('RecoverySplash', () => {
+ let props: React.ComponentProps
const mockToggleERWiz = vi.fn(() => Promise.resolve())
const mockProceedToRouteAndStep = vi.fn()
const mockRouteUpdateActions = {
proceedToRouteAndStep: mockProceedToRouteAndStep,
} as any
+ const mockMakeToast = vi.fn()
+ const mockResumeRecovery = vi.fn()
beforeEach(() => {
props = {
@@ -81,9 +92,14 @@ describe('RunPausedSplash', () => {
robotName: 'testRobot',
toggleERWizAsActiveUser: mockToggleERWiz,
routeUpdateActions: mockRouteUpdateActions,
+ recoveryActionMutationUtils: {
+ resumeRecovery: mockResumeRecovery,
+ } as any,
+ isWizardActive: false,
}
vi.mocked(StepInfo).mockReturnValue(MOCK STEP INFO
)
+ vi.mocked(useToaster).mockReturnValue({ makeToast: mockMakeToast } as any)
})
afterEach(() => {
@@ -147,4 +163,25 @@ describe('RunPausedSplash', () => {
expect(mockToggleERWiz).toHaveBeenCalledWith(true, true)
})
})
+
+ it('should render a door open toast if the door is open', () => {
+ props = {
+ ...props,
+ runStatus: RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
+ }
+
+ render(props)
+
+ clickButtonLabeled('Launch Recovery Mode')
+
+ expect(mockMakeToast).toHaveBeenCalled()
+ })
+
+ it(`should transition the run status from ${RUN_STATUS_AWAITING_RECOVERY_PAUSED} to ${RUN_STATUS_AWAITING_RECOVERY}`, () => {
+ props = { ...props, runStatus: RUN_STATUS_AWAITING_RECOVERY_PAUSED }
+
+ render(props)
+
+ expect(mockResumeRecovery).toHaveBeenCalled()
+ })
})
diff --git a/app/src/organisms/ErrorRecoveryFlows/index.tsx b/app/src/organisms/ErrorRecoveryFlows/index.tsx
index bb5dd9af584..b1b82fad236 100644
--- a/app/src/organisms/ErrorRecoveryFlows/index.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/index.tsx
@@ -18,7 +18,7 @@ import { useHost } from '@opentrons/react-api-client'
import { getIsOnDevice } from '../../redux/config'
import { ErrorRecoveryWizard, useERWizard } from './ErrorRecoveryWizard'
-import { RunPausedSplash, useRunPausedSplash } from './RunPausedSplash'
+import { RecoverySplash, useRecoverySplash } from './RecoverySplash'
import { RecoveryTakeover } from './RecoveryTakeover'
import {
useCurrentlyRecoveringFrom,
@@ -136,7 +136,7 @@ export function ErrorRecoveryFlows(
toggleERWizAsActiveUser,
} = useRecoveryTakeover(toggleERWizard)
const renderWizard = isActiveUser && (showERWizard || isDoorOpen)
- const showSplash = useRunPausedSplash(isOnDevice, renderWizard)
+ const showSplash = useRecoverySplash(isOnDevice, renderWizard)
const recoveryUtils = useERUtils({
...props,
@@ -168,7 +168,7 @@ export function ErrorRecoveryFlows(
/>
) : null}
{showSplash ? (
-
) : null}
>