Skip to content

Commit

Permalink
refactor(app): Add hook for on-the-fly maintenance run commands (#16249)
Browse files Browse the repository at this point in the history
Closes EXEC-699

In pre-release work, we decided to add a drop tip CTA option of "skip and home pipettes". In order to do this, we needed to create a maintenance run, initiate commands, and then close the maintenance context. There wasn't any general purpose way of doing this (we always had a wizard flow when issuing commands up to this point), so the solution was to commandeer existing drop tip wizard hooks to do this for us.

While that works, it has a few issues:

* It was pretty hackily implemented, and it kind of had to be without a dedicated hook.
* It makes following drop tip wizard more difficult to follow.
* If we every want to issue commands to a robot outside of a wizard flow, we have to add to the tech debt.
Since 8.1 should entail some drop tip wizard refactoring, this seems like a pretty good thing to refactor.

This adds a new useRobotControlCommands, which is a refactor of what the more specific drop tip wizard useDropTipMaintenanceRun. useDropTipMaintenanceRun was half doing this functionality plus other things (useDropTipMaintenanceRun now only does the other things!). This PR cleans up a lot of the cruft/paves the way for more drop tip refactors in 8.1 that were a result of the refactor.
  • Loading branch information
mjhuff authored Sep 13, 2024
1 parent a64a05c commit 6c48e3d
Show file tree
Hide file tree
Showing 35 changed files with 402 additions and 333 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ import { isTerminalRunStatus } from '../../utils'

import type { RobotType } from '@opentrons/shared-data'
import type { Run, RunStatus } from '@opentrons/api-client'
import type { DropTipWizardFlowsProps } from '../../../../../DropTipWizardFlows'
import type {
DropTipWizardFlowsProps,
PipetteWithTip,
} from '../../../../../DropTipWizardFlows'
import type { UseProtocolDropTipModalResult } from '../modals'
import type { PipetteDetails } from '../../../../../../resources/maintenance_runs'

export type RunHeaderDropTipWizProps =
| { showDTWiz: true; dtWizProps: DropTipWizardFlowsProps }
Expand Down Expand Up @@ -66,9 +70,7 @@ export function useRunHeaderDropTip({
toggleDTWiz,
isRunCurrent,
currentRunId: runId,
instrumentModelSpecs: aPipetteWithTip?.specs,
mount: aPipetteWithTip?.mount,
robotType,
pipetteInfo: buildPipetteDetails(aPipetteWithTip),
onSkipAndHome: () => {
closeCurrentRun()
},
Expand Down Expand Up @@ -133,3 +135,15 @@ export function useRunHeaderDropTip({

return { dropTipModalUtils, dropTipWizardUtils: buildDTWizUtils() }
}

// TODO(jh, 09-12-24): Consolidate this with the same utility that exists elsewhere.
function buildPipetteDetails(
aPipetteWithTip: PipetteWithTip | null
): PipetteDetails | null {
return aPipetteWithTip != null
? {
pipetteId: aPipetteWithTip.specs.name,
mount: aPipetteWithTip.mount,
}
: null
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ import {
} from '@opentrons/components'

import { TextOnlyButton } from '../../../../../../atoms/buttons'
import { useHomePipettes } from '../../../../../DropTipWizardFlows/hooks'
import { useHomePipettes } from '../../../../../DropTipWizardFlows'

import type { PipetteData } from '@opentrons/api-client'
import type { IconProps } from '@opentrons/components'
import type { UseHomePipettesProps } from '../../../../../DropTipWizardFlows/hooks'
import type { TipAttachmentStatusResult } from '../../../../../DropTipWizardFlows'
import type {
UseHomePipettesProps,
TipAttachmentStatusResult,
} from '../../../../../DropTipWizardFlows'

type UseProtocolDropTipModalProps = Pick<
UseHomePipettesProps,
'robotType' | 'instrumentModelSpecs' | 'mount'
'pipetteInfo'
> & {
areTipsAttached: TipAttachmentStatusResult['areTipsAttached']
toggleDTWiz: () => void
Expand All @@ -48,30 +50,28 @@ export function useProtocolDropTipModal({
toggleDTWiz,
isRunCurrent,
onSkipAndHome,
...homePipetteProps
pipetteInfo,
}: UseProtocolDropTipModalProps): UseProtocolDropTipModalResult {
const [showModal, setShowModal] = React.useState(areTipsAttached)

const { homePipettes, isHomingPipettes } = useHomePipettes({
...homePipetteProps,
isRunCurrent,
onHome: () => {
const { homePipettes, isHoming } = useHomePipettes({
pipetteInfo,
onSettled: () => {
onSkipAndHome()
setShowModal(false)
},
})

// Close the modal if a different app closes the run context.
React.useEffect(() => {
if (isRunCurrent && !isHomingPipettes) {
if (isRunCurrent && !isHoming) {
setShowModal(areTipsAttached)
} else if (!isRunCurrent) {
setShowModal(false)
}
}, [isRunCurrent, areTipsAttached, showModal]) // Continue to show the modal if a client dismisses the maintenance run on a different app.

const onSkip = (): void => {
homePipettes()
void homePipettes()
}

const onBeginRemoval = (): void => {
Expand All @@ -85,7 +85,7 @@ export function useProtocolDropTipModal({
modalProps: {
onSkip,
onBeginRemoval,
isDisabled: isHomingPipettes,
isDisabled: isHoming,
},
}
: { showModal: false, modalProps: null }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ import * as React from 'react'
import { describe, it, vi, expect, beforeEach } from 'vitest'
import { renderHook, act, screen, fireEvent } from '@testing-library/react'

import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data'

import { renderWithProviders } from '../../../../../../../__testing-utils__'
import { i18n } from '../../../../../../../i18n'
import { mockLeftSpecs } from '../../../../../../../redux/pipettes/__fixtures__'
import { useHomePipettes } from '../../../../../../DropTipWizardFlows/hooks'
import { useHomePipettes } from '../../../../../../DropTipWizardFlows'
import {
useProtocolDropTipModal,
ProtocolDropTipModal,
} from '../ProtocolDropTipModal'

import type { Mock } from 'vitest'

vi.mock('../../../../../../DropTipWizardFlows/hooks')
vi.mock('../../../../../../DropTipWizardFlows')

describe('useProtocolDropTipModal', () => {
let props: Parameters<typeof useProtocolDropTipModal>[0]
Expand All @@ -28,15 +25,17 @@ describe('useProtocolDropTipModal', () => {
isRunCurrent: true,
onSkipAndHome: vi.fn(),
currentRunId: 'MOCK_ID',
mount: 'left',
instrumentModelSpecs: mockLeftSpecs,
robotType: FLEX_ROBOT_TYPE,
pipetteInfo: {
pipetteId: '123',
pipetteName: 'MOCK_NAME',
mount: 'left',
},
}
mockHomePipettes = vi.fn()

vi.mocked(useHomePipettes).mockReturnValue({
homePipettes: mockHomePipettes,
isHomingPipettes: false,
isHoming: false,
})
})

Expand Down Expand Up @@ -96,7 +95,7 @@ describe('useProtocolDropTipModal', () => {
it('should set isDisabled to true when isHomingPipettes is true', () => {
vi.mocked(useHomePipettes).mockReturnValue({
homePipettes: mockHomePipettes,
isHomingPipettes: true,
isHoming: true,
})

const { result } = renderHook(() => useProtocolDropTipModal(props))
Expand Down
61 changes: 61 additions & 0 deletions app/src/organisms/DropTipWizardFlows/DropTipWizardFlows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as React from 'react'

import { useDropTipRouting, useDropTipWithType } from './hooks'
import { DropTipWizard } from './DropTipWizard'

import type { PipetteModelSpecs, RobotType } from '@opentrons/shared-data'
import type { PipetteData } from '@opentrons/api-client'
import type { FixitCommandTypeUtils, IssuedCommandsType } from './types'

/** Provides the user toggle for rendering Drop Tip Wizard Flows.
*
* NOTE: Rendering these flows is independent of whether tips are actually attached. First use useTipAttachmentStatus
* to get tip attachment status.
*/
export function useDropTipWizardFlows(): {
showDTWiz: boolean
toggleDTWiz: () => void
} {
const [showDTWiz, setShowDTWiz] = React.useState(false)

const toggleDTWiz = (): void => {
setShowDTWiz(!showDTWiz)
}

return { showDTWiz, toggleDTWiz }
}

export interface DropTipWizardFlowsProps {
robotType: RobotType
mount: PipetteData['mount']
instrumentModelSpecs: PipetteModelSpecs
/* isTakeover allows for optionally specifying a different callback if a different client cancels the "setup" type flow. */
closeFlow: (isTakeover?: boolean) => void
/* Optional. If provided, DT will issue "fixit" commands and render alternate Error Recovery compatible views. */
fixitCommandTypeUtils?: FixitCommandTypeUtils
}

export function DropTipWizardFlows(
props: DropTipWizardFlowsProps
): JSX.Element {
const { fixitCommandTypeUtils } = props

const issuedCommandsType: IssuedCommandsType =
fixitCommandTypeUtils != null ? 'fixit' : 'setup'

const dropTipWithTypeUtils = useDropTipWithType({
...props,
issuedCommandsType,
})

const dropTipRoutingUtils = useDropTipRouting(fixitCommandTypeUtils)

return (
<DropTipWizard
{...props}
{...dropTipWithTypeUtils}
{...dropTipRoutingUtils}
issuedCommandsType={issuedCommandsType}
/>
)
}
31 changes: 20 additions & 11 deletions app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,13 @@ import { useHomePipettes } from './hooks'

import type { HostConfig } from '@opentrons/api-client'
import type { OddModalHeaderBaseProps } from '../../molecules/OddModal/types'
import type { PipetteWithTip } from '.'
import type { UseHomePipettesProps } from './hooks'
import type { UseHomePipettesProps, PipetteWithTip } from './hooks'
import type { PipetteDetails } from '../../resources/maintenance_runs'

type TipsAttachedModalProps = Pick<
UseHomePipettesProps,
'robotType' | 'instrumentModelSpecs' | 'mount' | 'isRunCurrent'
> & {
type TipsAttachedModalProps = Pick<UseHomePipettesProps, 'onSettled'> & {
aPipetteWithTip: PipetteWithTip
host: HostConfig | null
setTipStatusResolved: (onEmpty?: () => void) => Promise<void>
onSkipAndHome: () => void
}

export const handleTipsAttachedModal = (
Expand All @@ -53,9 +49,10 @@ const TipsAttachedModal = NiceModal.create(

const { mount, specs } = aPipetteWithTip
const { showDTWiz, toggleDTWiz } = useDropTipWizardFlows()
const { homePipettes, isHomingPipettes } = useHomePipettes({
const { homePipettes, isHoming } = useHomePipettes({
...homePipetteProps,
onHome: () => {
pipetteInfo: buildPipetteDetails(aPipetteWithTip),
onSettled: () => {
modal.remove()
void setTipStatusResolved()
},
Expand Down Expand Up @@ -105,13 +102,13 @@ const TipsAttachedModal = NiceModal.create(
buttonType="secondary"
buttonText={t('skip_and_home_pipette')}
onClick={onHomePipettes}
disabled={isHomingPipettes}
disabled={isHoming}
/>
<SmallButton
flex="1"
buttonText={t('begin_removal')}
onClick={toggleDTWiz}
disabled={isHomingPipettes}
disabled={isHoming}
/>
</Flex>
</Flex>
Expand All @@ -130,3 +127,15 @@ const TipsAttachedModal = NiceModal.create(
)
}
)

// TODO(jh, 09-12-24): Consolidate this with the same utility that exists elsewhere.
function buildPipetteDetails(
aPipetteWithTip: PipetteWithTip | null
): PipetteDetails | null {
return aPipetteWithTip != null
? {
pipetteId: aPipetteWithTip.specs.name,
mount: aPipetteWithTip.mount,
}
: null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect, vi } from 'vitest'
import { renderHook, act } from '@testing-library/react'

import { useDropTipWizardFlows } from '..'

vi.mock('../DropTipWizard')
vi.mock('../hooks')

describe('useDropTipWizardFlows', () => {
it('should toggle showDTWiz state', () => {
const { result } = renderHook(() => useDropTipWizardFlows())

expect(result.current.showDTWiz).toBe(false)

act(() => {
result.current.toggleDTWiz()
})

expect(result.current.showDTWiz).toBe(true)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'

import { handleTipsAttachedModal } from '../TipsAttachedModal'
import { FLEX_ROBOT_TYPE, LEFT } from '@opentrons/shared-data'
import { LEFT } from '@opentrons/shared-data'
import { mockPipetteInfo } from '../../../redux/pipettes/__fixtures__'
import { useCloseCurrentRun } from '../../ProtocolUpload/hooks'
import { useDropTipWizardFlows } from '..'

import type { Mock } from 'vitest'
import type { PipetteModelSpecs } from '@opentrons/shared-data'
import type { HostConfig } from '@opentrons/api-client'
import type { Mock } from 'vitest'
import type { PipetteWithTip } from '..'
import type { PipetteWithTip } from '../hooks'

vi.mock('../../ProtocolUpload/hooks')
vi.mock('..')
Expand Down Expand Up @@ -52,11 +52,7 @@ const render = (aPipetteWithTip: PipetteWithTip) => {
host: MOCK_HOST,
aPipetteWithTip,
setTipStatusResolved: mockSetTipStatusResolved,
robotType: FLEX_ROBOT_TYPE,
mount: 'left',
instrumentModelSpecs: mockPipetteInfo.pipetteSpecs as any,
onSkipAndHome: vi.fn(),
isRunCurrent: true,
onSettled: vi.fn(),
})
}
data-testid="testButton"
Expand Down
Loading

0 comments on commit 6c48e3d

Please sign in to comment.