Skip to content

Commit 5338dce

Browse files
authored
refactor(app): Permit door open status during certain recovery steps (#16326)
Closes EXEC-700 Currently, when a user is on an error recovery step that contains copy like "do XYZ on the deck", opening the robot door causes the "door is open" modal to appear, which is a bit annoying when trying to read directions. While there are quite a few ways to solve this issue, in practice this requires a bit of thought when considering app takeovers, ODD/desktop app syncing, and operating within error recovery's current routing structure. This commit solves the above through a new RECOVERY_MAP_METADATA object, in which all steps specifically permit the door to be open or not. If allowed, we don't show the modal on that step, otherwise we do. Typescript enforces that any new route and steps added must specify whether the door is permitted to be open on that step. Yes, this may seem annoying, but I do think we want developers to think through whether the door should be open on every step. To handle robot commands that a user may initiate on a "door is permitted open" view, in which cases we absolutely DO want the door to be closed, we add to the wrapper that occurs before commands (now called handleMotionRouting) that ensures the door is closed, routing to the RecoveryDoorOpen modal if necessary and rejecting the command. After closing the door, the user is routed back to the view right before command execution. In effect, there are now two circumstances which cause RecoveryDoorOpen to appear: The old but adjusted behavior - the door is opened when it shouldn't be, and we render the door modal instead of the recovery content. When the door status is ok, we just render the recovery content. There is no change in the actual route. * Entirely new behavior - we manually adjust the route to the door modal and then route back to where we after the door status is resolved. In other words, the door open modal is recovery content. We do this when handling robot commands, as explained above. * While this may sound complicated, in practice it has little bearing. If we ever do multi-app syncing (same place in ER at the same time), this plays nicely with it. Lastly, this commit adds a cleanup util that resets state when another app takes over the recovery session. This is more pertinent now that door routing needs knowledge of the previous route, and we should reset this and some other state whenever a takeover happens. TLDR: As always, door logic is special-cased and requires special considerations.
1 parent 058854c commit 5338dce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+822
-298
lines changed

app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export type ErrorRecoveryWizardProps = ErrorRecoveryFlowsProps &
6464
ERUtilsResults & {
6565
robotType: RobotType
6666
isOnDevice: boolean
67-
isDoorOpen: boolean
6867
analytics: UseRecoveryAnalyticsResult<RecoveryRoute, RouteStep>
6968
failedCommand: ReturnType<typeof useRetainedFailedCommandBySource>
7069
}
@@ -95,10 +94,11 @@ export function ErrorRecoveryComponent(
9594
const {
9695
recoveryMap,
9796
hasLaunchedRecovery,
98-
isDoorOpen,
97+
doorStatusUtils,
9998
isOnDevice,
10099
analytics,
101100
} = props
101+
const { isProhibitedDoorOpen } = doorStatusUtils
102102
const { route, step } = recoveryMap
103103
const { t } = useTranslation('error_recovery')
104104
const { showModal, toggleModal } = useErrorDetailsModal()
@@ -135,15 +135,15 @@ export function ErrorRecoveryComponent(
135135

136136
// TODO(jh, 07-29-24): Make RecoveryDoorOpen render logic equivalent to RecoveryTakeover. Do not nest it in RecoveryWizard.
137137
const buildInterventionContent = (): JSX.Element => {
138-
if (isDoorOpen) {
138+
if (isProhibitedDoorOpen) {
139139
return <RecoveryDoorOpen {...props} />
140140
} else {
141141
return <ErrorRecoveryContent {...props} />
142142
}
143143
}
144144

145145
const isLargeDesktopStyle =
146-
!isDoorOpen &&
146+
!isProhibitedDoorOpen &&
147147
route === RECOVERY_MAP.DROP_TIP_FLOWS.ROUTE &&
148148
step !== RECOVERY_MAP.DROP_TIP_FLOWS.STEPS.BEGIN_REMOVAL
149149
const desktopType = isLargeDesktopStyle ? 'desktop-large' : 'desktop-small'
@@ -218,6 +218,10 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
218218
return <IgnoreErrorSkipStep {...props} />
219219
}
220220

221+
const buildManuallyRouteToDoorOpen = (): JSX.Element => {
222+
return <RecoveryDoorOpen {...props} />
223+
}
224+
221225
switch (props.recoveryMap.route) {
222226
case RECOVERY_MAP.OPTION_SELECTION.ROUTE:
223227
return buildSelectRecoveryOption()
@@ -248,6 +252,8 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
248252
case RECOVERY_MAP.ROBOT_PICKING_UP_TIPS.ROUTE:
249253
case RECOVERY_MAP.ROBOT_SKIPPING_STEP.ROUTE:
250254
return buildRecoveryInProgress()
255+
case RECOVERY_MAP.ROBOT_DOOR_OPEN.ROUTE:
256+
return buildManuallyRouteToDoorOpen()
251257
default:
252258
return buildSelectRecoveryOption()
253259
}
@@ -264,14 +270,14 @@ export function useInitialPipetteHome({
264270
routeUpdateActions,
265271
}: UseInitialPipetteHomeParams): void {
266272
const { homePipetteZAxes } = recoveryCommands
267-
const { setRobotInMotion } = routeUpdateActions
273+
const { handleMotionRouting } = routeUpdateActions
268274

269275
// Synchronously set the recovery route to "robot in motion" before initial render to prevent screen flicker on ER launch.
270276
React.useLayoutEffect(() => {
271277
if (hasLaunchedRecovery) {
272-
void setRobotInMotion(true)
278+
void handleMotionRouting(true)
273279
.then(() => homePipetteZAxes())
274-
.finally(() => setRobotInMotion(false))
280+
.finally(() => handleMotionRouting(false))
275281
}
276282
}, [hasLaunchedRecovery])
277283
}

app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,33 @@ import {
2323

2424
import type { RecoveryContentProps } from './types'
2525

26+
// There are two code paths that render this component:
27+
// 1) The door is open on a route & step in which it is not permitted to have the door open.
28+
// 2) The door is open on a route & step in which it is permitted to have the door open, but the app manually redirects
29+
// to this component. This is commonly done when the route & step itself allows the user to keep the door open, but some
30+
// action on that route & step is about to occur that requires the door to be closed. In this case, once the door event
31+
// has been satisfied, manually route back to the previous route & step.
2632
export function RecoveryDoorOpen({
2733
recoveryActionMutationUtils,
2834
runStatus,
35+
routeUpdateActions,
2936
}: RecoveryContentProps): JSX.Element {
3037
const {
3138
resumeRecovery,
3239
isResumeRecoveryLoading,
3340
} = recoveryActionMutationUtils
41+
const { stashedMap, proceedToRouteAndStep } = routeUpdateActions
3442
const { t } = useTranslation('error_recovery')
3543

44+
const primaryOnClick = (): void => {
45+
void resumeRecovery().then(() => {
46+
// See comments above for why we do this.
47+
if (stashedMap != null) {
48+
void proceedToRouteAndStep(stashedMap.route, stashedMap.step)
49+
}
50+
})
51+
}
52+
3653
return (
3754
<RecoverySingleColumnContentWrapper>
3855
<Flex
@@ -65,7 +82,7 @@ export function RecoveryDoorOpen({
6582
</Flex>
6683
<Flex justifyContent={JUSTIFY_END}>
6784
<RecoveryFooterButtons
68-
primaryBtnOnClick={resumeRecovery}
85+
primaryBtnOnClick={primaryOnClick}
6986
primaryBtnTextOverride={t('resume')}
7087
primaryBtnDisabled={
7188
runStatus === RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR

app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,15 @@ export function ErrorRecoveryFlowError({
5959
const { OPTION_SELECTION } = RECOVERY_MAP
6060
const { t } = useTranslation('error_recovery')
6161
const { selectedRecoveryOption } = currentRecoveryOptionUtils
62-
const { proceedToRouteAndStep, setRobotInMotion } = routeUpdateActions
62+
const { proceedToRouteAndStep, handleMotionRouting } = routeUpdateActions
6363
const { homePipetteZAxes } = recoveryCommands
6464

6565
const userRecoveryOptionCopy = getRecoveryOptionCopy(selectedRecoveryOption)
6666

6767
const onPrimaryClick = (): void => {
68-
void setRobotInMotion(true)
68+
void handleMotionRouting(true)
6969
.then(() => homePipetteZAxes())
70-
.finally(() => setRobotInMotion(false))
70+
.finally(() => handleMotionRouting(false))
7171
.then(() => proceedToRouteAndStep(OPTION_SELECTION.ROUTE))
7272
}
7373

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@ import {
2323
import { SelectRecoveryOption } from './SelectRecoveryOption'
2424

2525
import type { RecoveryContentProps } from '../types'
26-
import type {
27-
RecoveryTipStatusUtils,
28-
UseRecoveryCommandsResult,
29-
UseRouteUpdateActionsResult,
30-
} from '../hooks'
26+
import type { ERUtilsResults } from '../hooks'
3127

3228
export function CancelRun(props: RecoveryContentProps): JSX.Element {
3329
const { recoveryMap } = props
@@ -104,9 +100,9 @@ function CancelRunConfirmation({
104100
}
105101

106102
interface OnCancelRunProps {
107-
tipStatusUtils: RecoveryTipStatusUtils
108-
recoveryCommands: UseRecoveryCommandsResult
109-
routeUpdateActions: UseRouteUpdateActionsResult
103+
tipStatusUtils: ERUtilsResults['tipStatusUtils']
104+
recoveryCommands: ERUtilsResults['recoveryCommands']
105+
routeUpdateActions: ERUtilsResults['routeUpdateActions']
110106
}
111107

112108
// Manages routing to cancel route or drop tip route, depending on tip attachment status.
@@ -122,7 +118,7 @@ export function useOnCancelRun({
122118
} {
123119
const { ROBOT_CANCELING, DROP_TIP_FLOWS } = RECOVERY_MAP
124120
const { isLoadingTipStatus, areTipsAttached } = tipStatusUtils
125-
const { setRobotInMotion, proceedToRouteAndStep } = routeUpdateActions
121+
const { handleMotionRouting, proceedToRouteAndStep } = routeUpdateActions
126122
const { cancelRun } = recoveryCommands
127123

128124
const [hasUserClicked, setHasUserClicked] = React.useState(false)
@@ -135,7 +131,7 @@ export function useOnCancelRun({
135131
if (areTipsAttached) {
136132
void proceedToRouteAndStep(DROP_TIP_FLOWS.ROUTE)
137133
} else {
138-
void setRobotInMotion(true, ROBOT_CANCELING.ROUTE).then(() => {
134+
void handleMotionRouting(true, ROBOT_CANCELING.ROUTE).then(() => {
139135
cancelRun()
140136
})
141137
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function SkipToNextStep(
8181
currentRecoveryOptionUtils,
8282
} = props
8383
const {
84-
setRobotInMotion,
84+
handleMotionRouting,
8585
goBackPrevStep,
8686
proceedToRouteAndStep,
8787
} = routeUpdateActions
@@ -100,7 +100,7 @@ export function SkipToNextStep(
100100
}
101101

102102
const primaryBtnOnClick = (): Promise<void> => {
103-
return setRobotInMotion(true, ROBOT_SKIPPING_STEP.ROUTE).then(() => {
103+
return handleMotionRouting(true, ROBOT_SKIPPING_STEP.ROUTE).then(() => {
104104
skipFailedCommand()
105105
})
106106
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function BeginRemoval({
6666
const { aPipetteWithTip } = tipStatusUtils
6767
const {
6868
proceedNextStep,
69-
setRobotInMotion,
69+
handleMotionRouting,
7070
proceedToRouteAndStep,
7171
} = routeUpdateActions
7272
const { cancelRun } = recoveryCommands
@@ -85,7 +85,7 @@ export function BeginRemoval({
8585
RETRY_NEW_TIPS.STEPS.REPLACE_TIPS
8686
)
8787
} else {
88-
void setRobotInMotion(true, ROBOT_CANCELING.ROUTE).then(() => {
88+
void handleMotionRouting(true, ROBOT_CANCELING.ROUTE).then(() => {
8989
cancelRun()
9090
})
9191
}
@@ -153,7 +153,7 @@ function DropTipFlowsContainer(
153153
currentRecoveryOptionUtils,
154154
} = props
155155
const { DROP_TIP_FLOWS, ROBOT_CANCELING, RETRY_NEW_TIPS } = RECOVERY_MAP
156-
const { proceedToRouteAndStep, setRobotInMotion } = routeUpdateActions
156+
const { proceedToRouteAndStep, handleMotionRouting } = routeUpdateActions
157157
const { selectedRecoveryOption } = currentRecoveryOptionUtils
158158
const { setTipStatusResolved } = tipStatusUtils
159159
const { cancelRun } = recoveryCommands
@@ -172,7 +172,7 @@ function DropTipFlowsContainer(
172172
}
173173

174174
const onEmptyCache = (): void => {
175-
void setRobotInMotion(true, ROBOT_CANCELING.ROUTE).then(() => {
175+
void handleMotionRouting(true, ROBOT_CANCELING.ROUTE).then(() => {
176176
cancelRun()
177177
})
178178
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ export function RetryNewTips(props: RecoveryContentProps): JSX.Element {
4747
export function RetryWithNewTips(props: RecoveryContentProps): JSX.Element {
4848
const { recoveryCommands, routeUpdateActions } = props
4949
const { retryFailedCommand, resumeRun } = recoveryCommands
50-
const { setRobotInMotion } = routeUpdateActions
50+
const { handleMotionRouting } = routeUpdateActions
5151
const { ROBOT_RETRYING_STEP } = RECOVERY_MAP
5252
const { t } = useTranslation('error_recovery')
5353

5454
const primaryBtnOnClick = (): Promise<void> => {
55-
return setRobotInMotion(true, ROBOT_RETRYING_STEP.ROUTE)
55+
return handleMotionRouting(true, ROBOT_RETRYING_STEP.ROUTE)
5656
.then(() => retryFailedCommand())
5757
.then(() => {
5858
resumeRun()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export function RetrySameTips(props: RecoveryContentProps): JSX.Element {
3030
export function RetrySameTipsInfo(props: RecoveryContentProps): JSX.Element {
3131
const { routeUpdateActions, recoveryCommands } = props
3232
const { retryFailedCommand, resumeRun } = recoveryCommands
33-
const { setRobotInMotion } = routeUpdateActions
33+
const { handleMotionRouting } = routeUpdateActions
3434
const { ROBOT_RETRYING_STEP } = RECOVERY_MAP
3535
const { t } = useTranslation('error_recovery')
3636

3737
const primaryBtnOnClick = (): Promise<void> => {
38-
return setRobotInMotion(true, ROBOT_RETRYING_STEP.ROUTE)
38+
return handleMotionRouting(true, ROBOT_RETRYING_STEP.ROUTE)
3939
.then(() => retryFailedCommand())
4040
.then(() => {
4141
resumeRun()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ export function RetryStepInfo(props: RecoveryContentProps): JSX.Element {
3333
const { t } = useTranslation('error_recovery')
3434

3535
const { retryFailedCommand, resumeRun } = recoveryCommands
36-
const { setRobotInMotion } = routeUpdateActions
36+
const { handleMotionRouting } = routeUpdateActions
3737

3838
const primaryBtnOnClick = (): Promise<void> => {
39-
return setRobotInMotion(true, ROBOT_RETRYING_STEP.ROUTE)
39+
return handleMotionRouting(true, ROBOT_RETRYING_STEP.ROUTE)
4040
.then(() => retryFailedCommand())
4141
.then(() => {
4242
resumeRun()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ export function SkipStepNewTips(
4949
export function SkipStepWithNewTips(props: RecoveryContentProps): JSX.Element {
5050
const { recoveryCommands, routeUpdateActions } = props
5151
const { skipFailedCommand } = recoveryCommands
52-
const { setRobotInMotion } = routeUpdateActions
52+
const { handleMotionRouting } = routeUpdateActions
5353
const { ROBOT_SKIPPING_STEP } = RECOVERY_MAP
5454
const { t } = useTranslation('error_recovery')
5555

5656
const primaryBtnOnClick = (): Promise<void> => {
57-
return setRobotInMotion(true, ROBOT_SKIPPING_STEP.ROUTE).then(() => {
57+
return handleMotionRouting(true, ROBOT_SKIPPING_STEP.ROUTE).then(() => {
5858
skipFailedCommand()
5959
})
6060
}

0 commit comments

Comments
 (0)