Skip to content

Commit

Permalink
refactor(app): Add move animations to manual gripper move during Erro…
Browse files Browse the repository at this point in the history
…r Recovery (#16567)

Closes RQA-3392

Per design, when manually moving labware during a gripper error recovery flow, we should show the labware moving instead of stationary in the initial slot.
  • Loading branch information
mjhuff authored Oct 22, 2024
1 parent fe43efa commit 93aac9b
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
RUN_STATUS_RUNNING,
RUN_STATUS_STOP_REQUESTED,
} from '@opentrons/api-client'
import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware'
import { getLoadedLabwareDefinitionsByUri } from '@opentrons/shared-data'

import { renderWithProviders } from '/app/__testing-utils__'
import { i18n } from '/app/i18n'
Expand All @@ -33,7 +33,13 @@ vi.mock('/app/redux/config')
vi.mock('../RecoverySplash')
vi.mock('/app/redux-resources/analytics')
vi.mock('@opentrons/react-api-client')
vi.mock('/app/local-resources/labware')
vi.mock('@opentrons/shared-data', async () => {
const actual = await vi.importActual('@opentrons/shared-data')
return {
...actual,
getLoadedLabwareDefinitionsByUri: vi.fn(),
}
})
vi.mock('react-redux', async () => {
const actual = await vi.importActual('react-redux')
return {
Expand All @@ -45,7 +51,6 @@ vi.mock('react-redux', async () => {
describe('useErrorRecoveryFlows', () => {
beforeEach(() => {
vi.mocked(useCurrentlyRecoveringFrom).mockReturnValue('mockCommand' as any)
vi.mocked(getLabwareDefinitionsFromCommands).mockReturnValue([])
})

it('should have initial state of isERActive as false', () => {
Expand Down Expand Up @@ -143,7 +148,7 @@ describe('ErrorRecoveryFlows', () => {
runStatus: RUN_STATUS_AWAITING_RECOVERY,
failedCommandByRunRecord: mockFailedCommand,
runId: 'MOCK_RUN_ID',
protocolAnalysis: {} as any,
protocolAnalysis: null,
}
vi.mocked(ErrorRecoveryWizard).mockReturnValue(<div>MOCK WIZARD</div>)
vi.mocked(RecoverySplash).mockReturnValue(<div>MOCK RUN PAUSED SPLASH</div>)
Expand All @@ -168,6 +173,7 @@ describe('ErrorRecoveryFlows', () => {
intent: 'recovering',
showTakeover: false,
})
vi.mocked(getLoadedLabwareDefinitionsByUri).mockReturnValue({})
})

it('renders the wizard when showERWizard is true', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { screen, renderHook } from '@testing-library/react'

import { renderWithProviders } from '/app/__testing-utils__'
import { i18n } from '/app/i18n'
Expand Down Expand Up @@ -168,8 +168,8 @@ const TestWrapper = (props: GetRelevantLwLocationsParams) => {
const displayLocation = useRelevantFailedLwLocations(props)
return (
<>
<div>{`Current Loc: ${displayLocation.currentLoc}`}</div>
<div>{`New Loc: ${displayLocation.newLoc}`}</div>
<div>{`Current Loc: ${displayLocation.displayNameCurrentLoc}`}</div>
<div>{`New Loc: ${displayLocation.displayNameNewLoc}`}</div>
</>
)
}
Expand All @@ -181,7 +181,7 @@ const render = (props: ComponentProps<typeof TestWrapper>) => {
}

describe('useRelevantFailedLwLocations', () => {
const mockProtocolAnalysis = {} as any
const mockRunRecord = { data: { modules: [], labware: [] } } as any
const mockFailedLabware = {
location: { slotName: 'D1' },
} as any
Expand All @@ -194,14 +194,25 @@ describe('useRelevantFailedLwLocations', () => {
render({
failedLabware: mockFailedLabware,
failedCommandByRunRecord: mockFailedCommand,
protocolAnalysis: mockProtocolAnalysis,
runRecord: mockRunRecord,
})

screen.getByText('Current Loc: Slot D1')
screen.getByText('New Loc: null')

const { result } = renderHook(() =>
useRelevantFailedLwLocations({
failedLabware: mockFailedLabware,
failedCommandByRunRecord: mockFailedCommand,
runRecord: mockRunRecord,
})
)

expect(result.current.currentLoc).toStrictEqual({ slotName: 'D1' })
expect(result.current.newLoc).toBeNull()
})

it('should return current and new location for moveLabware commands', () => {
it('should return current and new locations for moveLabware commands', () => {
const mockFailedCommand = {
commandType: 'moveLabware',
params: {
Expand All @@ -212,10 +223,21 @@ describe('useRelevantFailedLwLocations', () => {
render({
failedLabware: mockFailedLabware,
failedCommandByRunRecord: mockFailedCommand,
protocolAnalysis: mockProtocolAnalysis,
runRecord: mockRunRecord,
})

screen.getByText('Current Loc: Slot D1')
screen.getByText('New Loc: Slot C2')

const { result } = renderHook(() =>
useRelevantFailedLwLocations({
failedLabware: mockFailedLabware,
failedCommandByRunRecord: mockFailedCommand,
runRecord: mockRunRecord,
})
)

expect(result.current.currentLoc).toStrictEqual({ slotName: 'D1' })
expect(result.current.newLoc).toStrictEqual({ slotName: 'C2' })
})
})
61 changes: 52 additions & 9 deletions app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useMemo } from 'react'

import {
getDeckDefFromRobotType,
getLoadedLabwareDefinitionsByUri,
getFixedTrashLabwareDefinition,
getModuleDef2,
getPositionFromSlotId,
Expand All @@ -11,6 +10,11 @@ import {
THERMOCYCLER_MODULE_V1,
} from '@opentrons/shared-data'

import {
getRunLabwareRenderInfo,
getRunModuleRenderInfo,
} from '/app/organisms/InterventionModal/utils'

import type { Run } from '@opentrons/api-client'
import type {
DeckDefinition,
Expand All @@ -22,21 +26,33 @@ import type {
LoadedLabware,
RobotType,
LabwareDefinitionsByUri,
LoadedModule,
} from '@opentrons/shared-data'
import type { ErrorRecoveryFlowsProps } from '..'
import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils'
import type {
RunLabwareInfo,
RunModuleInfo,
} from '/app/organisms/InterventionModal/utils'
import type { ERUtilsProps } from './useERUtils'

interface UseDeckMapUtilsProps {
runId: ErrorRecoveryFlowsProps['runId']
protocolAnalysis: ErrorRecoveryFlowsProps['protocolAnalysis']
failedLabwareUtils: UseFailedLabwareUtilsResult
labwareDefinitionsByUri: ERUtilsProps['labwareDefinitionsByUri']
runRecord?: Run
}

export interface UseDeckMapUtilsResult {
deckConfig: CutoutConfigProtocolSpec[]
modulesOnDeck: RunCurrentModulesOnDeck[]
labwareOnDeck: RunCurrentLabwareOnDeck[]
loadedLabware: LoadedLabware[]
loadedModules: LoadedModule[]
movedLabwareDef: LabwareDefinition2 | null
moduleRenderInfo: RunModuleInfo[]
labwareRenderInfo: RunLabwareInfo[]
highlightLabwareEventuallyIn: string[]
kind: 'intervention'
robotType: RobotType
Expand All @@ -47,19 +63,12 @@ export function useDeckMapUtils({
runRecord,
runId,
failedLabwareUtils,
labwareDefinitionsByUri,
}: UseDeckMapUtilsProps): UseDeckMapUtilsResult {
const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE
const deckConfig = getSimplestDeckConfigForProtocol(protocolAnalysis)
const deckDef = getDeckDefFromRobotType(robotType)

const labwareDefinitionsByUri = useMemo(
() =>
protocolAnalysis != null
? getLoadedLabwareDefinitionsByUri(protocolAnalysis?.commands)
: null,
[protocolAnalysis]
)

const currentModulesInfo = useMemo(
() =>
getRunCurrentModulesInfo({
Expand Down Expand Up @@ -93,6 +102,35 @@ export function useDeckMapUtils({
[runId, protocolAnalysis, runRecord, deckDef, failedLabwareUtils]
)

const movedLabwareDef =
labwareDefinitionsByUri != null && failedLabwareUtils.failedLabware != null
? labwareDefinitionsByUri[failedLabwareUtils.failedLabware.definitionUri]
: null

const moduleRenderInfo = useMemo(
() =>
runRecord != null && labwareDefinitionsByUri != null
? getRunModuleRenderInfo(
runRecord.data,
deckDef,
labwareDefinitionsByUri
)
: [],
[deckDef, labwareDefinitionsByUri, runRecord]
)

const labwareRenderInfo = useMemo(
() =>
runRecord != null && labwareDefinitionsByUri != null
? getRunLabwareRenderInfo(
runRecord.data,
labwareDefinitionsByUri,
deckDef
)
: [],
[deckDef, labwareDefinitionsByUri, runRecord]
)

return {
deckConfig,
modulesOnDeck: runCurrentModules.map(
Expand All @@ -112,6 +150,11 @@ export function useDeckMapUtils({
.filter(maybeSlot => maybeSlot != null) as string[],
kind: 'intervention',
robotType,
loadedModules: runRecord?.data.modules ?? [],
loadedLabware: runRecord?.data.labware ?? [],
movedLabwareDef,
moduleRenderInfo,
labwareRenderInfo,
}
}

Expand Down
9 changes: 8 additions & 1 deletion app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import { useShowDoorInfo } from './useShowDoorInfo'
import { useCleanupRecoveryState } from './useCleanupRecoveryState'
import { useFailedPipetteUtils } from './useFailedPipetteUtils'

import type { LabwareDefinition2, RobotType } from '@opentrons/shared-data'
import type {
LabwareDefinition2,
LabwareDefinitionsByUri,
RobotType,
} from '@opentrons/shared-data'
import type { IRecoveryMap, RouteStep, RecoveryRoute } from '../types'
import type { ErrorRecoveryFlowsProps } from '..'
import type { UseRouteUpdateActionsResult } from './useRouteUpdateActions'
Expand Down Expand Up @@ -48,6 +52,7 @@ export type ERUtilsProps = Omit<ErrorRecoveryFlowsProps, 'failedCommand'> & {
failedCommand: ReturnType<typeof useRetainedFailedCommandBySource>
showTakeover: boolean
allRunDefs: LabwareDefinition2[]
labwareDefinitionsByUri: LabwareDefinitionsByUri | null
}

export interface ERUtilsResults {
Expand Down Expand Up @@ -82,6 +87,7 @@ export function useERUtils({
runStatus,
showTakeover,
allRunDefs,
labwareDefinitionsByUri,
}: ERUtilsProps): ERUtilsResults {
const { data: attachedInstruments } = useInstrumentsQuery()
const { data: runRecord } = useNotifyRunQuery(runId)
Expand Down Expand Up @@ -168,6 +174,7 @@ export function useERUtils({
runRecord,
protocolAnalysis,
failedLabwareUtils,
labwareDefinitionsByUri,
})

const recoveryActionMutationUtils = useRecoveryActionMutation(
Expand Down
46 changes: 30 additions & 16 deletions app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
DispenseRunTimeCommand,
LiquidProbeRunTimeCommand,
MoveLabwareRunTimeCommand,
LabwareLocation,
} from '@opentrons/shared-data'
import type { LabwareDisplayLocationSlotOnly } from '/app/local-resources/labware'
import type { ErrorRecoveryFlowsProps } from '..'
Expand All @@ -40,8 +41,10 @@ interface UseFailedLabwareUtilsProps {
}

interface RelevantFailedLabwareLocations {
currentLoc: string
newLoc: string | null
displayNameCurrentLoc: string
displayNameNewLoc: string | null
currentLoc: LabwareLocation | null
newLoc: LabwareLocation | null
}

export type UseFailedLabwareUtilsResult = UseTipSelectionUtilsResult & {
Expand All @@ -53,6 +56,7 @@ export type UseFailedLabwareUtilsResult = UseTipSelectionUtilsResult & {
relevantWellName: string | null
/* The user-content nickname of the failed labware, if any */
failedLabwareNickname: string | null
/* Details relating to the labware location. */
failedLabwareLocations: RelevantFailedLabwareLocations
}

Expand Down Expand Up @@ -103,7 +107,7 @@ export function useFailedLabwareUtils({
const failedLabwareLocations = useRelevantFailedLwLocations({
failedLabware,
failedCommandByRunRecord,
protocolAnalysis,
runRecord,
})

return {
Expand Down Expand Up @@ -336,49 +340,59 @@ export function getRelevantWellName(

export type GetRelevantLwLocationsParams = Pick<
UseFailedLabwareUtilsProps,
'protocolAnalysis' | 'failedCommandByRunRecord'
'runRecord' | 'failedCommandByRunRecord'
> & {
failedLabware: UseFailedLabwareUtilsResult['failedLabware']
}

export function useRelevantFailedLwLocations({
failedLabware,
failedCommandByRunRecord,
protocolAnalysis,
runRecord,
}: GetRelevantLwLocationsParams): RelevantFailedLabwareLocations {
const { t } = useTranslation('protocol_command_text')

const BASE_DISPLAY_PARAMS: Omit<
LabwareDisplayLocationSlotOnly,
'location'
> = {
loadedLabwares: protocolAnalysis?.labware ?? [],
loadedModules: protocolAnalysis?.modules ?? [],
loadedLabwares: runRecord?.data?.labware ?? [],
loadedModules: runRecord?.data?.modules ?? [],
robotType: FLEX_ROBOT_TYPE,
t,
detailLevel: 'slot-only',
isOnDevice: false, // Always return the "slot XYZ" copy, which is the desktop copy.
}

const currentLocation = getLabwareDisplayLocation({
const displayNameCurrentLoc = getLabwareDisplayLocation({
...BASE_DISPLAY_PARAMS,
location: failedLabware?.location ?? null,
})

const getNewLocation = (): string | null => {
const getNewLocation = (): Pick<
RelevantFailedLabwareLocations,
'displayNameNewLoc' | 'newLoc'
> => {
switch (failedCommandByRunRecord?.commandType) {
case 'moveLabware':
return getLabwareDisplayLocation({
...BASE_DISPLAY_PARAMS,
location: failedCommandByRunRecord.params.newLocation,
})
return {
displayNameNewLoc: getLabwareDisplayLocation({
...BASE_DISPLAY_PARAMS,
location: failedCommandByRunRecord.params.newLocation,
}),
newLoc: failedCommandByRunRecord.params.newLocation,
}
default:
return null
return {
displayNameNewLoc: null,
newLoc: null,
}
}
}

return {
currentLoc: currentLocation,
newLoc: getNewLocation(),
displayNameCurrentLoc,
currentLoc: failedLabware?.location ?? null,
...getNewLocation(),
}
}
Loading

0 comments on commit 93aac9b

Please sign in to comment.