Skip to content

Commit 338c0a8

Browse files
authored
refactor(app): stacker latch jammed error recovery should ask user to confirm if hopper is not empty (#19148)
# Overview This PR implements a re-routing mechanism for the `flexStackerLabwareRetrieveFailed` error (or `STACKER_HOPPER_OR_SHUTTLE_EMPTY` for the frontend). # Changelog * change getErrorKind function to return `STACKER_HOPPER_OR_SHUTTLE_EMPTY` for the retrieve failed error * add `STACKER_HOPPER_OR_SHUTTLE_EMPTY` as a route with one single step `SELECT_FLOW` * add a screen for the `SELECT_FLOW` step to allow users to choose between two scenarios: "Stacker is empty" or "Stacker latch is jammed", which then routes to appropriate existing recovery flows * modify `SelectRecoveryOptionHome` function so we're automatically proceeding with the route if there's only one option
1 parent f2f2269 commit 338c0a8

File tree

11 files changed

+207
-8
lines changed

11 files changed

+207
-8
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,10 @@
170170
"view_recovery_options": "View recovery options",
171171
"you_can_still_drop_tips": "You can still drop the attached tips before proceeding to tip selection.",
172172
"ensure_stacker_shuttle_empty": "Ensure stacker labware shuttle is empty",
173-
"empty_shuttle_to_retry_retrieve": "Empty the labware shuttle so that the stacker is able to retry the retrieve command."
173+
"empty_shuttle_to_retry_retrieve": "Empty the labware shuttle so that the stacker is able to retry the retrieve command.",
174+
"stacker_is_empty": "Stacker is empty",
175+
"stacker_latch_is_jammed": "Stacker latch is jammed",
176+
"stacker_error": "Stacker error",
177+
"check_stacker": "Check the Stacker and verify current state",
178+
"stacker_what_is_wrong": "What is wrong with the Stacker?"
174179
}

app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
SkipStepSameTips,
2626
StackerHopperEmptyRetry,
2727
StackerHopperEmptySkip,
28+
StackerSelectErrorFlow,
2829
StackerShuttleEmptyRetry,
2930
StackerShuttleEmptySkip,
3031
StackerShuttleMissing,
@@ -263,6 +264,9 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
263264
const buildStackerStalledSkip = (): JSX.Element => {
264265
return <StackerStalledSkip {...props} />
265266
}
267+
const buildStackerSelectErrorFlow = (): JSX.Element => {
268+
return <StackerSelectErrorFlow {...props} />
269+
}
266270

267271
switch (props.recoveryMap.route) {
268272
case RECOVERY_MAP.OPTION_SELECTION.ROUTE:
@@ -293,6 +297,8 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
293297
return buildManualMoveLwAndSkip()
294298
case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE:
295299
return buildManualReplaceLwAndRetry()
300+
case RECOVERY_MAP.STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE:
301+
return buildStackerSelectErrorFlow()
296302
case RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE:
297303
return buildStackerHopperEmptyRetry()
298304
case RECOVERY_MAP.STACKER_HOPPER_EMPTY_SKIP.ROUTE:

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,23 @@ export function SelectRecoveryOptionHome({
6161

6262
useCurrentTipStatus(determineTipStatus)
6363

64+
const proceed = (): void => {
65+
analytics.reportActionSelectedEvent(selectedRoute)
66+
setSelectedRecoveryOption(selectedRoute)
67+
void proceedToRouteAndStep(selectedRoute as RecoveryRoute)
68+
}
69+
70+
if (validRecoveryOptions.length === 1) {
71+
// If there is only one valid recovery option, automatically proceed to that route
72+
proceed()
73+
}
74+
6475
return (
6576
<Flex css={CONTAINER_STYLE}>
6677
<RecoverySingleColumnContentWrapper
6778
css={CONTENT_WRAPPER_OVERRIDE_STYLE}
6879
footerDetails={{
69-
primaryBtnOnClick: () => {
70-
analytics.reportActionSelectedEvent(selectedRoute)
71-
setSelectedRecoveryOption(selectedRoute)
72-
void proceedToRouteAndStep(selectedRoute as RecoveryRoute)
73-
},
80+
primaryBtnOnClick: proceed,
7481
isSticky: true,
7582
}}
7683
>
@@ -199,6 +206,8 @@ export function getRecoveryOptions(errorKind: ErrorKind): RecoveryRoute[] {
199206
return STACKER_SHUTTLE_EMPTY_OPTIONS
200207
case ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED:
201208
return STACKER_SHUTTLE_OCCUPIED_OPTIONS
209+
case ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY:
210+
return [RECOVERY_MAP.STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE]
202211
}
203212
}
204213

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
4+
import {
5+
DIRECTION_COLUMN,
6+
Flex,
7+
RadioButton,
8+
SPACING,
9+
StyledText,
10+
} from '@opentrons/components'
11+
12+
import {
13+
ERROR_KINDS,
14+
ODD_SECTION_TITLE_STYLE,
15+
RECOVERY_MAP,
16+
} from '../constants'
17+
import {
18+
RecoveryFooterButtons,
19+
RecoverySingleColumnContentWrapper,
20+
} from '../shared'
21+
import {
22+
getRecoveryOptions,
23+
RecoveryOptions,
24+
SelectRecoveryOption,
25+
} from './SelectRecoveryOption'
26+
27+
import type { RecoveryContentProps, RecoveryRoute } from '../types'
28+
29+
type StackerErrorFlow =
30+
| typeof ERROR_KINDS.STACKER_HOPPER_EMPTY
31+
| typeof ERROR_KINDS.STACKER_SHUTTLE_EMPTY
32+
33+
const { STACKER_HOPPER_OR_SHUTTLE_EMPTY } = RECOVERY_MAP
34+
35+
export function StackerSelectErrorFlow(
36+
props: RecoveryContentProps
37+
): JSX.Element {
38+
const { recoveryMap } = props
39+
const { step, route } = recoveryMap
40+
41+
switch (step) {
42+
case STACKER_HOPPER_OR_SHUTTLE_EMPTY.STEPS.SELECT_FLOW:
43+
return <StackerHopperOrShuttleEmptyOptions {...props} />
44+
default:
45+
console.warn(
46+
`StackerSelectErrorFlow: ${step} in ${route} not explicitly handled. Rerouting.`
47+
)
48+
return <SelectRecoveryOption {...props} />
49+
}
50+
}
51+
52+
export function StackerHopperOrShuttleEmptyOptions(
53+
props: RecoveryContentProps
54+
): JSX.Element {
55+
const {
56+
routeUpdateActions,
57+
getRecoveryOptionCopy,
58+
isOnDevice,
59+
currentRecoveryOptionUtils,
60+
} = props
61+
const { proceedToRouteAndStep } = routeUpdateActions
62+
const { setSelectedRecoveryOption } = currentRecoveryOptionUtils
63+
const { t } = useTranslation('error_recovery')
64+
65+
const [showErrorOptions, setShowErrorOptions] = useState<boolean>(true)
66+
const [selectedError, setSelectedError] = useState<StackerErrorFlow>(
67+
ERROR_KINDS.STACKER_HOPPER_EMPTY
68+
)
69+
const [selectedRoute, setSelectedRoute] = useState<RecoveryRoute | undefined>(
70+
undefined
71+
)
72+
73+
const handlePrimaryClick = (): void => {
74+
if (showErrorOptions) {
75+
const options = getRecoveryOptions(selectedError)
76+
setSelectedRoute(options[0])
77+
setShowErrorOptions(false)
78+
} else if (selectedRoute != null) {
79+
setSelectedRecoveryOption(selectedRoute)
80+
void proceedToRouteAndStep(selectedRoute)
81+
}
82+
}
83+
84+
const handleSecondaryClick = (): void => {
85+
if (!showErrorOptions) {
86+
setShowErrorOptions(true)
87+
}
88+
}
89+
90+
const buildText = (): string => {
91+
if (showErrorOptions) {
92+
return isOnDevice ? t('check_stacker') : t('stacker_what_is_wrong')
93+
}
94+
return t('choose_a_recovery_action')
95+
}
96+
97+
const buildErrorSelection = (): JSX.Element => (
98+
<>
99+
<RadioButton
100+
key="stacker_empty_option"
101+
buttonLabel={t('stacker_is_empty')}
102+
buttonValue={ERROR_KINDS.STACKER_HOPPER_EMPTY}
103+
onChange={() => {
104+
setSelectedError(ERROR_KINDS.STACKER_HOPPER_EMPTY)
105+
}}
106+
isSelected={selectedError === ERROR_KINDS.STACKER_HOPPER_EMPTY}
107+
radioButtonType="large"
108+
largeDesktopBorderRadius={!isOnDevice}
109+
/>
110+
<RadioButton
111+
key="stacker_latch_jammed_option"
112+
buttonLabel={t('stacker_latch_is_jammed')}
113+
buttonValue={ERROR_KINDS.STACKER_SHUTTLE_EMPTY}
114+
onChange={() => {
115+
setSelectedError(ERROR_KINDS.STACKER_SHUTTLE_EMPTY)
116+
}}
117+
isSelected={selectedError === ERROR_KINDS.STACKER_SHUTTLE_EMPTY}
118+
radioButtonType="large"
119+
largeDesktopBorderRadius={!isOnDevice}
120+
/>
121+
</>
122+
)
123+
124+
const buildRecoveryOptions = (): JSX.Element => (
125+
<RecoveryOptions
126+
validRecoveryOptions={getRecoveryOptions(selectedError)}
127+
setSelectedRoute={setSelectedRoute}
128+
selectedRoute={selectedRoute}
129+
getRecoveryOptionCopy={getRecoveryOptionCopy}
130+
errorKind={selectedError}
131+
isOnDevice={isOnDevice}
132+
/>
133+
)
134+
135+
return (
136+
<RecoverySingleColumnContentWrapper>
137+
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
138+
<StyledText
139+
oddStyle="level4HeaderSemiBold"
140+
desktopStyle="headingSmallBold"
141+
css={ODD_SECTION_TITLE_STYLE}
142+
>
143+
{buildText()}
144+
</StyledText>
145+
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
146+
{showErrorOptions ? buildErrorSelection() : buildRecoveryOptions()}
147+
</Flex>
148+
</Flex>
149+
<RecoveryFooterButtons
150+
primaryBtnOnClick={handlePrimaryClick}
151+
secondaryBtnOnClick={
152+
!showErrorOptions ? handleSecondaryClick : undefined
153+
}
154+
/>
155+
</RecoverySingleColumnContentWrapper>
156+
)
157+
}

app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export { StackerShuttleEmptySkip } from './StackerShuttleEmptySkip'
1919
export { StackerShuttleMissing } from './StackerShuttleMissing'
2020
export { StackerStalledRetry } from './StackerStalledRetry'
2121
export { StackerStalledSkip } from './StackerStalledSkip'
22+
export { StackerSelectErrorFlow } from './StackerSelectErrorFlow'

app/src/organisms/ErrorRecoveryFlows/constants.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const ERROR_KINDS = {
4949
STACKER_SHUTTLE_MISSING: 'STACKER_SHUTTLE_MISSING',
5050
STACKER_SHUTTLE_EMPTY: 'STACKER_SHUTTLE_EMPTY',
5151
STACKER_SHUTTLE_OCCUPIED: 'STACKER_SHUTTLE_OCCUPIED',
52+
STACKER_HOPPER_OR_SHUTTLE_EMPTY: 'STACKER_HOPPER_OR_SHUTTLE_EMPTY',
5253
} as const
5354

5455
export const STACKER_ERROR_KINDS: ErrorKind[] = [
@@ -57,6 +58,7 @@ export const STACKER_ERROR_KINDS: ErrorKind[] = [
5758
ERROR_KINDS.STACKER_HOPPER_EMPTY,
5859
ERROR_KINDS.STACKER_SHUTTLE_EMPTY,
5960
ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED,
61+
ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY,
6062
] as const
6163

6264
// TODO(jh, 06-14-24): Consolidate motion routes to a single route with several steps.
@@ -225,6 +227,12 @@ export const RECOVERY_MAP = {
225227
SKIP: 'skip',
226228
},
227229
},
230+
STACKER_HOPPER_OR_SHUTTLE_EMPTY: {
231+
ROUTE: 'stacker-hopper-or-shuttle-empty',
232+
STEPS: {
233+
SELECT_FLOW: 'select-flow',
234+
},
235+
},
228236
STACKER_HOPPER_EMPTY_RETRY: {
229237
ROUTE: 'stacker-hopper-empty-retry',
230238
STEPS: {
@@ -346,6 +354,7 @@ const {
346354
STACKER_HOPPER_EMPTY_SKIP,
347355
STACKER_SHUTTLE_EMPTY_RETRY,
348356
STACKER_SHUTTLE_EMPTY_SKIP,
357+
STACKER_HOPPER_OR_SHUTTLE_EMPTY,
349358
} = RECOVERY_MAP
350359

351360
// The deterministic ordering of steps for a given route.
@@ -440,6 +449,9 @@ export const STEP_ORDER: StepOrder = {
440449
STACKER_SHUTTLE_MISSING_RETRY.STEPS.ENSURE_SHUTTLE_EMPTY,
441450
STACKER_SHUTTLE_MISSING_RETRY.STEPS.RETRY,
442451
],
452+
[STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE]: [
453+
STACKER_HOPPER_OR_SHUTTLE_EMPTY.STEPS.SELECT_FLOW,
454+
],
443455
[STACKER_HOPPER_EMPTY_RETRY.ROUTE]: [
444456
STACKER_HOPPER_EMPTY_RETRY.STEPS.FILL_HOPPER,
445457
STACKER_HOPPER_EMPTY_RETRY.STEPS.ENSURE_SHUTTLE_EMPTY,
@@ -660,6 +672,11 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = {
660672
},
661673
[STACKER_STALLED_SKIP.STEPS.SKIP]: { allowDoorOpen: false },
662674
},
675+
[STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE]: {
676+
[STACKER_HOPPER_OR_SHUTTLE_EMPTY.STEPS.SELECT_FLOW]: {
677+
allowDoorOpen: false,
678+
},
679+
},
663680
[STACKER_HOPPER_EMPTY_RETRY.ROUTE]: {
664681
[STACKER_HOPPER_EMPTY_RETRY.STEPS.FILL_HOPPER]: {
665682
allowDoorOpen: true,

app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ describe('handleRecoveryOptionAction', () => {
321321
RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE,
322322
RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.ROUTE,
323323
RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.ROUTE,
324+
RECOVERY_MAP.STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE,
324325
]
325326

326327
// Routes that should return no toasts.

app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export function useErrorName(errorKind: ErrorKind): string {
3535
return t('labware_not_retrieved')
3636
case ERROR_KINDS.STACKER_SHUTTLE_OCCUPIED:
3737
return t('stacker_shuttle_not_empty')
38+
case ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY:
39+
return t('stacker_error')
3840
default:
3941
return t('error')
4042
}

app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ export function handleRecoveryOptionAction<T>(
185185
case RECOVERY_MAP.STACKER_HOPPER_EMPTY_RETRY.ROUTE:
186186
case RECOVERY_MAP.STACKER_SHUTTLE_EMPTY_RETRY.ROUTE:
187187
case RECOVERY_MAP.STACKER_SHUTTLE_MISSING_RETRY.ROUTE:
188+
case RECOVERY_MAP.STACKER_HOPPER_OR_SHUTTLE_EMPTY.ROUTE:
188189
return currentStepReturnVal
189190
default: {
190191
console.error('Unhandled recovery toast case. Handle explicitly.')

app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('getErrorKind', () => {
7070
{
7171
commandType: 'flexStacker/retrieve',
7272
errorType: DEFINED_ERROR_TYPES.STACKER_SHUTTLE_EMPTY,
73-
expectedError: ERROR_KINDS.STACKER_SHUTTLE_EMPTY,
73+
expectedError: ERROR_KINDS.STACKER_HOPPER_OR_SHUTTLE_EMPTY,
7474
},
7575
{
7676
commandType: 'flexStacker/retrieve',

0 commit comments

Comments
 (0)