Skip to content

Commit df62b9b

Browse files
authored
refactor(app, components): Update "run paused" splash (#15262)
Closes EXEC-398 and EXEC-447 Refactors the Splash page, changing functionality and aligning it more with current Hi-Fi designs.
1 parent 162d12e commit df62b9b

33 files changed

+888
-517
lines changed

api-client/src/runs/commands/types.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,3 @@ export interface CreateCommandParams {
4747
timeout?: number
4848
failedCommandId?: string
4949
}
50-
51-
export interface RunCommandError {
52-
id: string
53-
errorType: string
54-
createdAt: string
55-
detail: string
56-
}

app/src/assets/localization/en/error_recovery.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@
1010
"general_error_message": "<Placeholder>",
1111
"go_back": "Go back",
1212
"how_do_you_want_to_proceed": "How do you want to proceed?",
13+
"launch_recovery_mode": "Launch Recovery Mode",
1314
"recovery_mode": "Recovery Mode",
1415
"recovery_mode_explanation": "<block>Recovery Mode provides you with guided and manual controls for handling errors at runtime.</block><br/><block>You can make changes to ensure the step in progress when the error occurred can be completed or choose to cancel the protocol. When changes are made and no subsequent errors are detected, the method completes. Depending on the conditions that caused the error, you will only be provided with appropriate options.</block>",
15-
"resume": "Resume",
16+
"retry_step": "Retry step",
1617
"run_paused": "Run paused",
1718
"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.",
1819
"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.",
1920
"stand_back": "Stand back, robot is in motion",
2021
"stand_back_resuming": "Stand back, resuming current step",
2122
"stand_back_retrying": "Stand back, retrying current command",
23+
"tip_not_detected": "Tip not detected",
2224
"view_recovery_options": "View recovery options"
2325
}

app/src/atoms/buttons/LargeButton.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface LargeButtonProps extends StyleProps {
2222
buttonType?: LargeButtonTypes
2323
buttonText: React.ReactNode
2424
iconName?: IconName
25+
iconColorOverride?: string
2526
subtext?: string
2627
disabled?: boolean
2728
}
@@ -31,6 +32,7 @@ export function LargeButton(props: LargeButtonProps): JSX.Element {
3132
buttonType = 'primary',
3233
buttonText,
3334
iconName,
35+
iconColorOverride,
3436
subtext,
3537
disabled = false,
3638
...buttonProps
@@ -130,7 +132,8 @@ export function LargeButton(props: LargeButtonProps): JSX.Element {
130132
color={
131133
disabled
132134
? COLORS.grey50
133-
: LARGE_BUTTON_PROPS_BY_TYPE[buttonType].iconColor
135+
: iconColorOverride ??
136+
LARGE_BUTTON_PROPS_BY_TYPE[buttonType].iconColor
134137
}
135138
size="5rem"
136139
/>

app/src/atoms/buttons/__tests__/LargeButton.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,15 @@ describe('LargeButton', () => {
5353
render(props)
5454
expect(screen.getByRole('button')).toBeDisabled()
5555
})
56+
57+
it('renders the icon override color if specified', () => {
58+
props = {
59+
...props,
60+
iconColorOverride: COLORS.red50,
61+
}
62+
render(props)
63+
expect(screen.getByLabelText('play-round-corners icon')).toHaveStyle(
64+
`color: ${COLORS.red50}`
65+
)
66+
})
5667
})

app/src/organisms/DropTipWizard/utils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { AlertPrimaryButton, SPACING } from '@opentrons/components'
66
import { DROP_TIP_SPECIAL_ERROR_TYPES } from './constants'
77
import { SmallButton } from '../../atoms/buttons'
88

9-
import type { RunCommandError } from '@opentrons/api-client'
9+
import type { RunCommandError } from '@opentrons/shared-data'
1010
import type { useChainMaintenanceCommands } from '../../resources/runs'
1111

1212
export interface ErrorDetails {

app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,54 +13,75 @@ import {
1313
import { getIsOnDevice } from '../../redux/config'
1414
import { getTopPortalEl } from '../../App/portal'
1515
import { BeforeBeginning } from './BeforeBeginning'
16-
import { SelectRecoveryOption, ResumeRun, CancelRun } from './RecoveryOptions'
16+
import { SelectRecoveryOption, RetryStep, CancelRun } from './RecoveryOptions'
1717
import { ErrorRecoveryHeader } from './ErrorRecoveryHeader'
1818
import { RecoveryInProgress } from './RecoveryInProgress'
19-
import { getErrorKind, useRouteUpdateActions } from './utils'
20-
import { useRecoveryCommands } from './useRecoveryCommands'
19+
import { getErrorKind } from './utils'
2120
import { RECOVERY_MAP } from './constants'
2221

2322
import type { FailedCommand, IRecoveryMap, RecoveryContentProps } from './types'
23+
import type {
24+
useRouteUpdateActions,
25+
UseRouteUpdateActionsResult,
26+
} from './utils'
27+
import type {
28+
useRecoveryCommands,
29+
UseRecoveryCommandsResult,
30+
} from './useRecoveryCommands'
31+
32+
interface UseERWizardResult {
33+
hasLaunchedRecovery: boolean
34+
showERWizard: boolean
35+
toggleERWizard: (hasLaunchedER: boolean) => Promise<void>
36+
}
2437

25-
export interface ErrorRecoveryFlowsProps {
26-
runId: string
27-
failedCommand: FailedCommand | null
38+
export function useERWizard(): UseERWizardResult {
39+
const [showERWizard, setShowERWizard] = React.useState(false)
40+
// Because RunPausedSplash has access to some ER Wiz routes but is not a part of the ER wizard, the splash screen
41+
// is the "home" route as opposed to SelectRecoveryOption (accessed by pressing "go back" or "continue" enough times)
42+
// when recovery mode has not been launched.
43+
const [hasLaunchedRecovery, setHasLaunchedRecovery] = React.useState(false)
44+
45+
const toggleERWizard = (hasLaunchedER: boolean): Promise<void> => {
46+
setHasLaunchedRecovery(hasLaunchedER)
47+
setShowERWizard(!showERWizard)
48+
return Promise.resolve()
49+
}
50+
51+
return { showERWizard, toggleERWizard, hasLaunchedRecovery }
2852
}
2953

30-
export function ErrorRecoveryWizard({
31-
runId,
32-
failedCommand,
33-
}: ErrorRecoveryFlowsProps): JSX.Element {
34-
/**
35-
* Recovery Route: A logically-related collection of recovery steps or a single step if unrelated to any existing recovery route.
36-
* Recovery Step: Analogous to a "step" in other wizard flows.
37-
*/
38-
const [recoveryMap, setRecoveryMap] = React.useState<IRecoveryMap>({
39-
route: RECOVERY_MAP.OPTION_SELECTION.ROUTE,
40-
step: RECOVERY_MAP.OPTION_SELECTION.STEPS.SELECT,
41-
})
54+
export interface ErrorRecoveryWizardProps {
55+
failedCommand: FailedCommand | null
56+
recoveryMap: IRecoveryMap
57+
routeUpdateActions: UseRouteUpdateActionsResult
58+
recoveryCommands: UseRecoveryCommandsResult
59+
hasLaunchedRecovery: boolean
60+
}
4261

62+
export function ErrorRecoveryWizard(
63+
props: ErrorRecoveryWizardProps
64+
): JSX.Element {
65+
const {
66+
hasLaunchedRecovery,
67+
failedCommand,
68+
recoveryCommands,
69+
routeUpdateActions,
70+
} = props
4371
const errorKind = getErrorKind(failedCommand?.error?.errorType)
4472
const isOnDevice = useSelector(getIsOnDevice)
45-
const routeUpdateActions = useRouteUpdateActions({
46-
recoveryMap,
47-
setRecoveryMap,
48-
})
49-
const recoveryCommands = useRecoveryCommands({
50-
runId,
51-
failedCommand,
52-
})
5373

54-
useInitialPipetteHome(recoveryCommands, routeUpdateActions)
74+
useInitialPipetteHome({
75+
hasLaunchedRecovery,
76+
recoveryCommands,
77+
routeUpdateActions,
78+
})
5579

5680
return (
5781
<ErrorRecoveryComponent
58-
failedCommand={failedCommand}
5982
errorKind={errorKind}
6083
isOnDevice={isOnDevice}
61-
recoveryMap={recoveryMap}
62-
routeUpdateActions={routeUpdateActions}
63-
recoveryCommands={recoveryCommands}
84+
{...props}
6485
/>
6586
)
6687
}
@@ -98,7 +119,7 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
98119
}
99120

100121
const buildResumeRun = (): JSX.Element => {
101-
return <ResumeRun {...props} />
122+
return <RetryStep {...props} />
102123
}
103124

104125
const buildCancelRun = (): JSX.Element => {
@@ -110,7 +131,7 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
110131
return buildBeforeBeginning()
111132
case RECOVERY_MAP.OPTION_SELECTION.ROUTE:
112133
return buildSelectRecoveryOption()
113-
case RECOVERY_MAP.RESUME.ROUTE:
134+
case RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE:
114135
return buildResumeRun()
115136
case RECOVERY_MAP.CANCEL_RUN.ROUTE:
116137
return buildCancelRun()
@@ -123,19 +144,26 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
123144
return buildSelectRecoveryOption()
124145
}
125146
}
126-
127-
// Home the Z-axis of all attached pipettes on Error Recovery launch.
128-
export function useInitialPipetteHome(
129-
recoveryCommands: ReturnType<typeof useRecoveryCommands>,
147+
interface UseInitialPipetteHomeParams {
148+
hasLaunchedRecovery: boolean
149+
recoveryCommands: ReturnType<typeof useRecoveryCommands>
130150
routeUpdateActions: ReturnType<typeof useRouteUpdateActions>
131-
): void {
151+
}
152+
// Home the Z-axis of all attached pipettes on Error Recovery launch.
153+
export function useInitialPipetteHome({
154+
hasLaunchedRecovery,
155+
recoveryCommands,
156+
routeUpdateActions,
157+
}: UseInitialPipetteHomeParams): void {
132158
const { homePipetteZAxes } = recoveryCommands
133159
const { setRobotInMotion } = routeUpdateActions
134160

135161
// Synchronously set the recovery route to "robot in motion" before initial render to prevent screen flicker on ER launch.
136162
React.useLayoutEffect(() => {
137-
void setRobotInMotion(true)
138-
.then(() => homePipetteZAxes())
139-
.finally(() => setRobotInMotion(false))
140-
}, [])
163+
if (hasLaunchedRecovery) {
164+
void setRobotInMotion(true)
165+
.then(() => homePipetteZAxes())
166+
.finally(() => setRobotInMotion(false))
167+
}
168+
}, [hasLaunchedRecovery])
141169
}

app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ResumeRun.tsx renamed to app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { RecoveryFooterButtons } from './shared'
1616

1717
import type { RecoveryContentProps } from '../types'
1818

19-
export function ResumeRun({
19+
export function RetryStep({
2020
isOnDevice,
2121
routeUpdateActions,
2222
recoveryCommands,

app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ export function SelectRecoveryOption({
5757
primaryBtnOnClick={() =>
5858
proceedToRoute(selectedRoute as RecoveryRoute)
5959
}
60-
secondaryBtnOnClick={() =>
61-
proceedToRoute(RECOVERY_MAP.BEFORE_BEGINNING.ROUTE)
62-
}
6360
/>
6461
</Flex>
6562
)
@@ -83,8 +80,8 @@ export function RecoveryOptions({
8380
return validRecoveryOptions.map((recoveryOption: RecoveryRoute) => {
8481
const buildOptionName = (): string => {
8582
switch (recoveryOption) {
86-
case RECOVERY_MAP.RESUME.ROUTE:
87-
return t('resume')
83+
case RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE:
84+
return t('retry_step')
8885
case RECOVERY_MAP.CANCEL_RUN.ROUTE:
8986
return t('cancel_run')
9087
default:
@@ -113,6 +110,6 @@ export function getRecoveryOptions(errorKind: ErrorKind): RecoveryRoute[] {
113110
}
114111

115112
export const GENERAL_ERROR_OPTIONS: RecoveryRoute[] = [
116-
RECOVERY_MAP.RESUME.ROUTE,
113+
RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE,
117114
RECOVERY_MAP.CANCEL_RUN.ROUTE,
118115
]

app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { screen, fireEvent, waitFor } from '@testing-library/react'
44

55
import { renderWithProviders } from '../../../../__testing-utils__'
66
import { i18n } from '../../../../i18n'
7+
import { mockRecoveryContentProps } from '../../__fixtures__'
78
import { CancelRun } from '../CancelRun'
8-
import { RECOVERY_MAP, ERROR_KINDS } from '../../constants'
9+
import { RECOVERY_MAP } from '../../constants'
910

1011
import type { Mock } from 'vitest'
1112

@@ -25,10 +26,7 @@ describe('RecoveryFooterButtons', () => {
2526
const mockRouteUpdateActions = { goBackPrevStep: mockGoBackPrevStep } as any
2627

2728
props = {
28-
isOnDevice: true,
29-
recoveryCommands: {} as any,
30-
failedCommand: {} as any,
31-
errorKind: ERROR_KINDS.GENERAL_ERROR,
29+
...mockRecoveryContentProps,
3230
routeUpdateActions: mockRouteUpdateActions,
3331
recoveryMap: {
3432
route: CANCEL_RUN.ROUTE,

app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ResumeRun.test.tsx renamed to app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,33 @@ import { screen, fireEvent, waitFor } from '@testing-library/react'
44

55
import { renderWithProviders } from '../../../../__testing-utils__'
66
import { i18n } from '../../../../i18n'
7-
import { ResumeRun } from '../ResumeRun'
8-
import { RECOVERY_MAP, ERROR_KINDS } from '../../constants'
7+
import { mockRecoveryContentProps } from '../../__fixtures__'
8+
import { RetryStep } from '../RetryStep'
9+
import { RECOVERY_MAP } from '../../constants'
910

1011
import type { Mock } from 'vitest'
1112

12-
const render = (props: React.ComponentProps<typeof ResumeRun>) => {
13-
return renderWithProviders(<ResumeRun {...props} />, {
13+
const render = (props: React.ComponentProps<typeof RetryStep>) => {
14+
return renderWithProviders(<RetryStep {...props} />, {
1415
i18nInstance: i18n,
1516
})[0]
1617
}
1718

1819
describe('RecoveryFooterButtons', () => {
19-
const { RESUME, ROBOT_RETRYING_COMMAND } = RECOVERY_MAP
20-
let props: React.ComponentProps<typeof ResumeRun>
20+
const { RETRY_FAILED_COMMAND, ROBOT_RETRYING_COMMAND } = RECOVERY_MAP
21+
let props: React.ComponentProps<typeof RetryStep>
2122
let mockGoBackPrevStep: Mock
2223

2324
beforeEach(() => {
2425
mockGoBackPrevStep = vi.fn()
2526
const mockRouteUpdateActions = { goBackPrevStep: mockGoBackPrevStep } as any
2627

2728
props = {
28-
isOnDevice: true,
29-
recoveryCommands: {} as any,
30-
failedCommand: {} as any,
31-
errorKind: ERROR_KINDS.GENERAL_ERROR,
29+
...mockRecoveryContentProps,
3230
routeUpdateActions: mockRouteUpdateActions,
3331
recoveryMap: {
34-
route: RESUME.ROUTE,
35-
step: RESUME.STEPS.CONFIRM_RESUME,
32+
route: RETRY_FAILED_COMMAND.ROUTE,
33+
step: RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY,
3634
},
3735
}
3836
})

0 commit comments

Comments
 (0)