From 446cb2ecb4d1e57b63f29c0df359dd4fdd19f6d3 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 2 Aug 2024 13:36:46 -0400 Subject: [PATCH] feat(app): Implement `hasEverEnteredErrorRecovery` (#15876) Closes EXEC-636 and EXEC-637 and RQA-2900 Because ER always requires the users to go through drop tip flows, it's redundant and mildly annoying for users to see a second set of now irrelevant CTAs. Now, we only show the CTAs at the end of the run if the run did not enter error recovery (and the conditions for detecting potential tip attachment occur). --- .../Devices/ProtocolRun/ProtocolRunHeader.tsx | 16 +++++++---- app/src/pages/RunSummary/index.tsx | 28 ++++++++++++++----- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index a35276c7e8c..9f4bef400ee 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -164,7 +164,6 @@ export function ProtocolRunHeader({ const { startedAt, stoppedAt, completedAt } = useRunTimestamps(runId) const [showRunFailedModal, setShowRunFailedModal] = React.useState(false) const [showDropTipBanner, setShowDropTipBanner] = React.useState(true) - const [enteredER, setEnteredER] = React.useState(false) const isResetRunLoadingRef = React.useRef(false) const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const highestPriorityError = @@ -224,6 +223,8 @@ export function ProtocolRunHeader({ isMostRecentRunCurrent: mostRecentRunId === runId, }) + const enteredER = runRecord?.data.hasEverEnteredErrorRecovery + React.useEffect(() => { if (isFlex) { if (runStatus === RUN_STATUS_IDLE) { @@ -232,7 +233,8 @@ export function ProtocolRunHeader({ } else if ( runStatus != null && // @ts-expect-error runStatus expected to possibly not be terminal - RUN_STATUSES_TERMINAL.includes(runStatus) + RUN_STATUSES_TERMINAL.includes(runStatus) && + enteredER === false ) { void determineTipStatus() } @@ -245,9 +247,14 @@ export function ProtocolRunHeader({ } }, [protocolData, isRobotViewable, navigate]) + React.useEffect(() => { + if (isRunCurrent && typeof enteredER === 'boolean') { + reportRecoveredRunResult(runStatus, enteredER) + } + }, [isRunCurrent, enteredER]) + // Side effects dependent on the current run state. React.useEffect(() => { - reportRecoveredRunResult(runStatus, enteredER) // After a user-initiated stopped run, close the run current run automatically. if (runStatus === RUN_STATUS_STOPPED && isRunCurrent && runId != null) { trackProtocolRunEvent({ @@ -258,9 +265,6 @@ export function ProtocolRunHeader({ }) closeCurrentRun() } - if (runStatus === RUN_STATUS_AWAITING_RECOVERY) { - setEnteredER(true) - } }, [runStatus, isRunCurrent, runId, closeCurrentRun]) const startedAtTimestamp = diff --git a/app/src/pages/RunSummary/index.tsx b/app/src/pages/RunSummary/index.tsx index ab08e986544..b47c1838164 100644 --- a/app/src/pages/RunSummary/index.tsx +++ b/app/src/pages/RunSummary/index.tsx @@ -65,6 +65,7 @@ import { formatTimeWithUtcLabel, useNotifyRunQuery } from '../../resources/runs' import { handleTipsAttachedModal } from '../../organisms/DropTipWizardFlows/TipsAttachedModal' import { useMostRecentRunId } from '../../organisms/ProtocolUpload/hooks/useMostRecentRunId' import { useTipAttachmentStatus } from '../../organisms/DropTipWizardFlows' +import { useRecoveryAnalytics } from '../../organisms/ErrorRecoveryFlows/hooks' import type { OnDeviceRouteParams } from '../../App/types' import type { PipetteWithTip } from '../../organisms/DropTipWizardFlows' @@ -109,7 +110,6 @@ export function RunSummary(): JSX.Element { ) const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name ?? 'no name' - const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const onCloneRunSuccess = (): void => { if (isQuickTransfer) { @@ -117,10 +117,23 @@ export function RunSummary(): JSX.Element { } } + const { trackProtocolRunEvent } = useTrackProtocolRunEvent( + runId, + robotName as string + ) + const robotAnalyticsData = useRobotAnalyticsData(robotName as string) + const { reportRecoveredRunResult } = useRecoveryAnalytics() + + const enteredER = runRecord?.data.hasEverEnteredErrorRecovery + React.useEffect(() => { + if (isRunCurrent && typeof enteredER === 'boolean') { + reportRecoveredRunResult(runStatus, enteredER) + } + }, [isRunCurrent, enteredER]) + const { reset, isResetRunLoading } = useRunControls(runId, onCloneRunSuccess) const trackEvent = useTrackEvent() const { closeCurrentRun, isClosingCurrentRun } = useCloseCurrentRun() - const robotAnalyticsData = useRobotAnalyticsData(robotName) const [showRunFailedModal, setShowRunFailedModal] = React.useState( false ) @@ -151,10 +164,12 @@ export function RunSummary(): JSX.Element { isFlex: true, }) - // Determine tip status on initial render only. + // Determine tip status on initial render only. Error Recovery always handles tip status, so don't show it twice. React.useEffect(() => { - determineTipStatus() - }, []) + if (isRunCurrent && enteredER === false) { + void determineTipStatus() + } + }, [isRunCurrent, enteredER]) // TODO(jh, 08-02-24): Revisit useCurrentRunRoute and top level redirects. const queryClient = useQueryClient() @@ -164,7 +179,6 @@ export function RunSummary(): JSX.Element { queryClient.setQueryData([host, 'runs', runId, 'details'], () => undefined) navigate('/') } - // TODO(jh, 07-24-24): After EXEC-504, add reportRecoveredRunResult here. const returnToQuickTransfer = (): void => { if (!isRunCurrent) { @@ -215,7 +229,7 @@ export function RunSummary(): JSX.Element { } const handleRunAgain = (pipettesWithTip: PipetteWithTip[]): void => { - if (isRunCurrent && pipettesWithTip.length > 0) { + if (mostRecentRunId === runId && pipettesWithTip.length > 0) { void handleTipsAttachedModal({ setTipStatusResolved: setTipStatusResolvedAndRoute(handleRunAgain), host,