diff --git a/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx b/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx index 329ec38d199..0586db9966c 100644 --- a/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx +++ b/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx @@ -324,10 +324,15 @@ export const DropTipWizardContent = ( const handleProceed = (): void => { if (currentStep === BLOWOUT_SUCCESS) { void proceedToRoute(DT_ROUTES.DROP_TIP) - } else if (tipDropComplete != null) { - tipDropComplete() } else { - proceedWithConditionalClose() + // Clear the error recovery submap upon completion of drop tip wizard. + fixitCommandTypeUtils?.reportMap(null) + + if (tipDropComplete != null) { + tipDropComplete() + } else { + proceedWithConditionalClose() + } } } diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardHeader.test.tsx b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardHeader.test.tsx index 4a3cc939eff..5dbb85ecca2 100644 --- a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardHeader.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizardHeader.test.tsx @@ -39,7 +39,7 @@ describe('useSeenBlowoutSuccess', () => { it('should not render step counter when currentRoute is BEFORE_BEGINNING', () => { const { result } = renderHook(() => useSeenBlowoutSuccess({ - currentStep: 'SOME_STEP', + currentStep: 'SOME_STEP' as any, currentRoute: DT_ROUTES.BEFORE_BEGINNING, currentStepIdx: 0, }) diff --git a/app/src/organisms/DropTipWizardFlows/constants.ts b/app/src/organisms/DropTipWizardFlows/constants.ts index 1a6e9c24e04..39d75318824 100644 --- a/app/src/organisms/DropTipWizardFlows/constants.ts +++ b/app/src/organisms/DropTipWizardFlows/constants.ts @@ -9,17 +9,17 @@ export const POSITION_AND_DROP_TIP = 'POSITION_AND_DROP_TIP' as const export const DROP_TIP_SUCCESS = 'DROP_TIP_SUCCESS' as const export const INVALID = 'INVALID' as const -const BEFORE_BEGINNING_STEPS = [BEFORE_BEGINNING] -const BLOWOUT_STEPS = [ +export const BEFORE_BEGINNING_STEPS = [BEFORE_BEGINNING] as const +export const BLOWOUT_STEPS = [ CHOOSE_BLOWOUT_LOCATION, POSITION_AND_BLOWOUT, BLOWOUT_SUCCESS, -] -const DROP_TIP_STEPS = [ +] as const +export const DROP_TIP_STEPS = [ CHOOSE_DROP_TIP_LOCATION, POSITION_AND_DROP_TIP, DROP_TIP_SUCCESS, -] +] as const export const DT_ROUTES = { BEFORE_BEGINNING: BEFORE_BEGINNING_STEPS, diff --git a/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useDropTipRouting.test.tsx b/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useDropTipRouting.test.tsx index 350c7bc7a4f..651b3959b5d 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useDropTipRouting.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useDropTipRouting.test.tsx @@ -87,12 +87,12 @@ describe('useDropTipRouting', () => { }) }) -describe('useExternalMapUpdates', () => { - it('should call trackCurrentMap when the drop tip flow map updates', async () => { - const mockTrackCurrentMap = vi.fn() +describe('useReportMap', () => { + it('should call reportMap when the drop tip flow map updates', async () => { + const mockReportMap = vi.fn() const mockFixitUtils = { - trackCurrentMap: mockTrackCurrentMap, + reportMap: mockReportMap, } as any const { result } = renderHook(() => useDropTipRouting(mockFixitUtils)) @@ -101,18 +101,18 @@ describe('useExternalMapUpdates', () => { await result.current.proceedToRoute(DT_ROUTES.BLOWOUT) }) - expect(mockTrackCurrentMap).toHaveBeenCalledWith({ - currentRoute: DT_ROUTES.BLOWOUT, - currentStep: expect.any(String), + expect(mockReportMap).toHaveBeenCalledWith({ + route: DT_ROUTES.BLOWOUT, + step: expect.any(String), }) await act(async () => { await result.current.proceed() }) - expect(mockTrackCurrentMap).toHaveBeenCalledWith({ - currentRoute: DT_ROUTES.BLOWOUT, - currentStep: expect.any(String), + expect(mockReportMap).toHaveBeenCalledWith({ + route: DT_ROUTES.BLOWOUT, + step: expect.any(String), }) }) }) @@ -126,9 +126,7 @@ describe('getInitialRouteAndStep', () => { }) it('should return the default initial route and step when fixitUtils.routeOverride is not provided', () => { - const fixitUtils = { - routeOverride: undefined, - } as any + const fixitUtils = undefined const [initialRoute, initialStep] = getInitialRouteAndStep(fixitUtils) @@ -138,12 +136,12 @@ describe('getInitialRouteAndStep', () => { it('should return the overridden route and step when fixitUtils.routeOverride is provided', () => { const fixitUtils = { - routeOverride: DT_ROUTES.DROP_TIP, + routeOverride: { route: DT_ROUTES.DROP_TIP, step: DT_ROUTES.DROP_TIP[2] }, } as any const [initialRoute, initialStep] = getInitialRouteAndStep(fixitUtils) expect(initialRoute).toBe(DT_ROUTES.DROP_TIP) - expect(initialStep).toBe(DT_ROUTES.DROP_TIP[0]) + expect(initialStep).toBe(DT_ROUTES.DROP_TIP[2]) }) }) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipRouting.tsx b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipRouting.tsx index 78e3e63977e..50a72417c8b 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipRouting.tsx +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipRouting.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import head from 'lodash/head' import last from 'lodash/last' -import { DT_ROUTES, INVALID } from '../constants' +import { BEFORE_BEGINNING_STEPS, DT_ROUTES, INVALID } from '../constants' import type { DropTipFlowsRoute, @@ -46,7 +46,7 @@ export function useDropTipRouting( ): UseDropTipRoutingResult { const [initialRoute, initialStep] = React.useMemo( () => getInitialRouteAndStep(fixitUtils), - [fixitUtils] + [] ) const [dropTipFlowsMap, setDropTipFlowsMap] = React.useState( @@ -57,7 +57,7 @@ export function useDropTipRouting( } ) - useExternalMapUpdates(dropTipFlowsMap, fixitUtils) + useReportMap(dropTipFlowsMap, fixitUtils) const { currentStep, currentRoute } = dropTipFlowsMap @@ -126,7 +126,7 @@ interface DropTipRouteNavigationResult { // Returns functions that calculate the next and previous steps of a route given a step. function getDropTipRouteNavigation( - route: DropTipFlowsStep[] + route: readonly DropTipFlowsStep[] ): DropTipRouteNavigationResult { const getNextStep = (step: DropTipFlowsStep): StepNavigationResult => { const isStepFinalStep = step === last(route) @@ -180,7 +180,7 @@ function determineValidRoute( } // If an external flow is keeping track of the Drop tip flow map, update it when the drop tip flow map updates. -export function useExternalMapUpdates( +export function useReportMap( map: DropTipFlowsMap, fixitUtils?: FixitCommandTypeUtils ): void { @@ -188,9 +188,9 @@ export function useExternalMapUpdates( React.useEffect(() => { if (fixitUtils != null) { - fixitUtils.trackCurrentMap({ currentRoute, currentStep }) + fixitUtils.reportMap({ route: currentRoute, step: currentStep }) } - }, [currentStep, currentRoute, fixitUtils]) + }, [currentStep, currentRoute]) } // If present, return fixit route overrides for setting the initial Drop Tip Wizard route. @@ -198,8 +198,8 @@ export function getInitialRouteAndStep( fixitUtils?: FixitCommandTypeUtils ): [DropTipFlowsRoute, DropTipFlowsStep] { const routeOverride = fixitUtils?.routeOverride - const initialRoute = routeOverride ?? DT_ROUTES.BEFORE_BEGINNING - const initialStep = head(routeOverride) ?? head(DT_ROUTES.BEFORE_BEGINNING) + const initialRoute = routeOverride?.route ?? DT_ROUTES.BEFORE_BEGINNING + const initialStep = routeOverride?.step ?? BEFORE_BEGINNING_STEPS[0] - return [initialRoute as DropTipFlowsRoute, initialStep as DropTipFlowsStep] + return [initialRoute, initialStep] } diff --git a/app/src/organisms/DropTipWizardFlows/types.ts b/app/src/organisms/DropTipWizardFlows/types.ts index f4aa36266ae..15a9e25cc9e 100644 --- a/app/src/organisms/DropTipWizardFlows/types.ts +++ b/app/src/organisms/DropTipWizardFlows/types.ts @@ -1,11 +1,9 @@ import type { DT_ROUTES } from './constants' import type { DropTipErrorComponents } from './hooks' import type { DropTipWizardProps } from './DropTipWizard' -import type { ERUtilsResults } from '../ErrorRecoveryFlows/hooks' export type DropTipFlowsRoute = typeof DT_ROUTES[keyof typeof DT_ROUTES] export type DropTipFlowsStep = DropTipFlowsRoute[number] - export interface ErrorDetails { message: string header?: string @@ -30,14 +28,21 @@ interface ButtonOverrides { tipDropComplete: (() => void) | null } +export interface DropTipWizardRouteOverride { + route: DropTipFlowsRoute + step: DropTipFlowsStep | null +} + export interface FixitCommandTypeUtils { runId: string failedCommandId: string - trackCurrentMap: ERUtilsResults['trackExternalMap'] copyOverrides: CopyOverrides errorOverrides: ErrorOverrides buttonOverrides: ButtonOverrides - routeOverride?: typeof DT_ROUTES[keyof typeof DT_ROUTES] + /* Report to an external flow (ex, Error Recovery) the current step of drop tip wizard. */ + reportMap: (dropTipMap: DropTipWizardRouteOverride | null) => void + /* If supplied, begin drop tip flows on the specified route & step. If no step is supplied, begin at the start of the route. */ + routeOverride?: DropTipWizardRouteOverride } export type DropTipWizardContainerProps = DropTipWizardProps & { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index 6dbc6924559..e5886839326 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -200,7 +200,7 @@ export function useDropTipFlowUtils({ tipStatusUtils, failedCommand, currentRecoveryOptionUtils, - trackExternalMap, + subMapUtils, routeUpdateActions, recoveryMap, }: RecoveryContentProps): FixitCommandTypeUtils { @@ -215,6 +215,7 @@ export function useDropTipFlowUtils({ const { step } = recoveryMap const { selectedRecoveryOption } = currentRecoveryOptionUtils const { proceedToRouteAndStep } = routeUpdateActions + const { updateSubMap, subMap } = subMapUtils const failedCommandId = failedCommand?.id ?? '' // We should have a failed command here unless the run is not in AWAITING_RECOVERY. const buildTipDropCompleteBtn = (): string => { @@ -288,12 +289,18 @@ export function useDropTipFlowUtils({ } // If a specific step within the DROP_TIP_FLOWS route is selected, begin the Drop Tip Flows at its related route. + // + // NOTE: The substep is cleared by drop tip wizard after the completion of the wizard flow. const buildRouteOverride = (): FixitCommandTypeUtils['routeOverride'] => { + if (subMap?.route != null) { + return { route: subMap.route, step: subMap.step } + } + switch (step) { case DROP_TIP_FLOWS.STEPS.CHOOSE_TIP_DROP: - return DT_ROUTES.DROP_TIP + return { route: DT_ROUTES.DROP_TIP, step: subMap?.step ?? null } case DROP_TIP_FLOWS.STEPS.CHOOSE_BLOWOUT: - return DT_ROUTES.BLOWOUT + return { route: DT_ROUTES.BLOWOUT, step: subMap?.step ?? null } } } @@ -301,9 +308,9 @@ export function useDropTipFlowUtils({ runId, failedCommandId, copyOverrides: buildCopyOverrides(), - trackCurrentMap: trackExternalMap, errorOverrides: buildErrorOverrides(), buttonOverrides: buildButtonOverrides(), routeOverride: buildRouteOverride(), + reportMap: updateSubMap, } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx index 362d30e2860..bd743bc60e7 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx @@ -69,6 +69,7 @@ describe('ManageTips', () => { currentRecoveryOptionUtils: { selectedRecoveryOption: null, } as any, + subMapUtils: { subMap: null, updateSubMap: vi.fn() }, } vi.mocked(DropTipWizardFlows).mockReturnValue( @@ -176,13 +177,16 @@ describe('useDropTipFlowUtils', () => { const mockRunId = 'MOCK_RUN_ID' const mockTipStatusUtils = { runId: mockRunId } const mockProceedToRouteAndStep = vi.fn() + const mockUpdateSubMap = vi.fn() const { ERROR_WHILE_RECOVERING, DROP_TIP_FLOWS } = RECOVERY_MAP const mockProps = { tipStatusUtils: mockTipStatusUtils, failedCommand: null, - previousRoute: null, - trackExternalMap: vi.fn(), + subMapUtils: { + updateSubMap: mockUpdateSubMap, + subMap: null, + }, currentRecoveryOptionUtils: { selectedRecoveryOption: null, } as any, @@ -225,19 +229,13 @@ describe('useDropTipFlowUtils', () => { screen.getByText('Proceed to cancel') }) - it('should call trackExternalMap with the current map', () => { - const mockTrackExternalMap = vi.fn() - const { result } = renderHook(() => - useDropTipFlowUtils({ - ...mockProps, - trackExternalMap: mockTrackExternalMap, - }) - ) + it('should call updateSubMap with the current map', () => { + const { result } = renderHook(() => useDropTipFlowUtils(mockProps)) - const currentMap = { route: 'route', step: 'step' } - result.current.trackCurrentMap(currentMap) + const currentMap = { route: 'route', step: 'step' } as any + result.current.reportMap(currentMap) - expect(mockTrackExternalMap).toHaveBeenCalledWith(currentMap) + expect(mockUpdateSubMap).toHaveBeenCalledWith(currentMap) }) it('should return the correct error overrides', () => { @@ -296,19 +294,43 @@ describe('useDropTipFlowUtils', () => { ) }) - it(`should return correct route overrides when the route is ${DROP_TIP_FLOWS.STEPS.CHOOSE_TIP_DROP}`, () => { + it(`should return correct route override when the step is ${DROP_TIP_FLOWS.STEPS.CHOOSE_TIP_DROP}`, () => { const { result } = renderHook(() => useDropTipFlowUtils(mockProps)) - expect(result.current.routeOverride).toEqual(DT_ROUTES.DROP_TIP) + expect(result.current.routeOverride).toEqual({ + route: DT_ROUTES.DROP_TIP, + step: null, + }) }) - it(`should return correct route overrides when the route is ${DROP_TIP_FLOWS.STEPS.CHOOSE_BLOWOUT}`, () => { + it(`should return correct route override when the step is ${DROP_TIP_FLOWS.STEPS.CHOOSE_BLOWOUT}`, () => { const mockPropsBlowout = { ...mockProps, recoveryMap: { step: DROP_TIP_FLOWS.STEPS.CHOOSE_BLOWOUT }, } const { result } = renderHook(() => useDropTipFlowUtils(mockPropsBlowout)) - expect(result.current.routeOverride).toEqual(DT_ROUTES.BLOWOUT) + expect(result.current.routeOverride).toEqual({ + route: DT_ROUTES.BLOWOUT, + step: null, + }) + }) + + it('should use subMap.step in routeOverride if available', () => { + const mockPropsWithSubMap = { + ...mockProps, + subMapUtils: { + ...mockProps.subMapUtils, + subMap: { route: DT_ROUTES.DROP_TIP, step: 'SOME_STEP' }, + }, + } + const { result } = renderHook(() => + useDropTipFlowUtils(mockPropsWithSubMap) + ) + + expect(result.current.routeOverride).toEqual({ + route: DT_ROUTES.DROP_TIP, + step: 'SOME_STEP', + }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts index e7d3a85c484..919f45d9c42 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts @@ -73,7 +73,7 @@ export const mockRecoveryContentProps: RecoveryContentProps = { deckMapUtils: { setSelectedLocation: () => {} } as any, stepCounts: {} as any, protocolAnalysis: mockRobotSideAnalysis, - trackExternalMap: () => null, + subMapUtils: { subMap: null, updateSubMap: () => null } as any, hasLaunchedRecovery: true, getRecoveryOptionCopy: () => 'MOCK_COPY', commandsAfterFailedCommand: [ diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index 965abf761bc..10860cbacc5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -26,7 +26,10 @@ import type { UseRecoveryCommandsResult } from './useRecoveryCommands' import type { RecoveryTipStatusUtils } from './useRecoveryTipStatus' import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils' import type { UseDeckMapUtilsResult } from './useDeckMapUtils' -import type { CurrentRecoveryOptionUtils } from './useRecoveryRouting' +import type { + CurrentRecoveryOptionUtils, + SubMapUtils, +} from './useRecoveryRouting' import type { RecoveryActionMutationResult } from './useRecoveryActionMutation' import type { StepCounts } from '../../../resources/protocols/hooks' import type { UseRecoveryAnalyticsResult } from './useRecoveryAnalytics' @@ -52,9 +55,9 @@ export interface ERUtilsResults { recoveryActionMutationUtils: RecoveryActionMutationResult failedPipetteInfo: PipetteData | null hasLaunchedRecovery: boolean - trackExternalMap: (map: Record) => void stepCounts: StepCounts commandsAfterFailedCommand: ReturnType + subMapUtils: SubMapUtils } const SUBSEQUENT_COMMAND_DEPTH = 2 @@ -86,8 +89,8 @@ export function useERUtils({ const { recoveryMap, setRM, - trackExternalMap, currentRecoveryOptionUtils, + ...subMapUtils } = useRecoveryRouting() const recoveryToastUtils = useRecoveryToasts({ @@ -155,7 +158,7 @@ export function useERUtils({ ) return { recoveryMap, - trackExternalMap, + subMapUtils, currentRecoveryOptionUtils, recoveryActionMutationUtils, routeUpdateActions, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryRouting.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryRouting.ts index db3daed9976..b97a1206739 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryRouting.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryRouting.ts @@ -2,30 +2,38 @@ import * as React from 'react' import { RECOVERY_MAP } from '../constants' -import type { IRecoveryMap, RecoveryRoute } from '../types' -import type { ERUtilsResults } from './useERUtils' +import type { IRecoveryMap, RecoveryRoute, ValidSubMap } from '../types' + +// Utils for getting/setting the current submap. See useRecoveryRouting. +export interface SubMapUtils { + /* See useRecoveryRouting. */ + updateSubMap: (subMap: ValidSubMap | null) => void + /* See useRecoveryRouting. */ + subMap: ValidSubMap | null +} + +export interface UseRecoveryRoutingResult { + recoveryMap: IRecoveryMap + currentRecoveryOptionUtils: CurrentRecoveryOptionUtils + setRM: (map: IRecoveryMap) => void + updateSubMap: SubMapUtils['updateSubMap'] + subMap: SubMapUtils['subMap'] +} /** * ER Wizard routing. Also provides access to the routing of any other flow launched from ER. * Recovery Route: A logically-related collection of recovery steps or a single step if unrelated to any existing recovery route. * Recovery Step: Analogous to a "step" in other wizard flows. + * SubMap: Used for more granular routing, when required. * - * @params {trackExternalStep} Used to keep track of the current step in other flows launched from Error Recovery, ex. Drop Tip flows. */ - -export function useRecoveryRouting(): { - recoveryMap: IRecoveryMap - currentRecoveryOptionUtils: CurrentRecoveryOptionUtils - setRM: (map: IRecoveryMap) => void - trackExternalMap: ERUtilsResults['trackExternalMap'] -} { +export function useRecoveryRouting(): UseRecoveryRoutingResult { const [recoveryMap, setRecoveryMap] = React.useState({ route: RECOVERY_MAP.OPTION_SELECTION.ROUTE, step: RECOVERY_MAP.OPTION_SELECTION.STEPS.SELECT, }) - // If we do multi-app routing, concat the sub-step to the error recovery routing. - const [, setSubMap] = React.useState | null>(null) + const [subMap, setSubMap] = React.useState(null) const currentRecoveryOptionUtils = useSelectedRecoveryOption() @@ -33,7 +41,8 @@ export function useRecoveryRouting(): { recoveryMap, currentRecoveryOptionUtils, setRM: setRecoveryMap, - trackExternalMap: setSubMap, + updateSubMap: setSubMap, + subMap, } } diff --git a/app/src/organisms/ErrorRecoveryFlows/types.ts b/app/src/organisms/ErrorRecoveryFlows/types.ts index 747000f2dbb..f3df4a86c50 100644 --- a/app/src/organisms/ErrorRecoveryFlows/types.ts +++ b/app/src/organisms/ErrorRecoveryFlows/types.ts @@ -1,6 +1,10 @@ import type { RunCommandSummary } from '@opentrons/api-client' import type { ERROR_KINDS, RECOVERY_MAP, INVALID } from './constants' import type { ErrorRecoveryWizardProps } from './ErrorRecoveryWizard' +import type { + DropTipFlowsRoute, + DropTipFlowsStep, +} from '../DropTipWizardFlows/types' export type FailedCommand = RunCommandSummary export type InvalidStep = typeof INVALID @@ -20,6 +24,13 @@ interface RecoveryMapDetails { STEP_ORDER: RouteStep } +export type ValidSubRoutes = DropTipFlowsRoute +export type ValidSubSteps = DropTipFlowsStep +export interface ValidSubMap { + route: ValidSubRoutes + step: ValidSubSteps | null +} + export type RecoveryMap = Record export type StepOrder = { [K in RecoveryRoute]: RouteStep[]