From 9d06d9c96b26014ba8966d46a8b29c845bf16de6 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 13 Oct 2020 18:28:29 -0400 Subject: [PATCH] feat(app): update pipette attach flow to include calibration When attaching a pipette, if you do not currently have a pipette offset calibration stored for that combination of pipette serial number and mount, attach pipette flow will directly feed into the pipette offset calibration flow (which may also include tip length cal for the tip used in pipette offset cal) Closes #2130 --- .../useCalibratePipetteOffset.test.js | 34 ++-- .../useCalibratePipetteOffset.js | 38 ++++- .../ChangePipette/ConfirmPipette.js | 13 +- app/src/components/ChangePipette/index.js | 64 +++++--- .../InstrumentSettings/PipetteInfo.js | 2 +- .../PipetteOffsetCalibrationControl.js | 154 ------------------ 6 files changed, 95 insertions(+), 210 deletions(-) delete mode 100644 app/src/components/InstrumentSettings/PipetteOffsetCalibrationControl.js diff --git a/app/src/components/CalibratePipetteOffset/__tests__/useCalibratePipetteOffset.test.js b/app/src/components/CalibratePipetteOffset/__tests__/useCalibratePipetteOffset.test.js index 03f99cc5128c..d91c3afd27ba 100644 --- a/app/src/components/CalibratePipetteOffset/__tests__/useCalibratePipetteOffset.test.js +++ b/app/src/components/CalibratePipetteOffset/__tests__/useCalibratePipetteOffset.test.js @@ -1,6 +1,5 @@ // @flow import * as React from 'react' -import { Provider } from 'react-redux' import uniqueId from 'lodash/uniqueId' import { mountWithStore } from '@opentrons/components/__utils__' import { act } from 'react-dom/test-utils' @@ -10,7 +9,6 @@ import * as Sessions from '../../../sessions' import { mockPipetteOffsetCalibrationSessionAttributes } from '../../../sessions/__fixtures__' import { useCalibratePipetteOffset } from '../useCalibratePipetteOffset' -import { mount } from 'enzyme' import type { State } from '../../../types' import type { SessionType } from '../../../sessions' @@ -32,13 +30,20 @@ const mockGetRequestById: JestMockFn< describe('useCalibratePipetteOffset hook', () => { let startCalibration let CalWizardComponent - let store const robotName = 'robotName' const mountString = 'left' + const onComplete = jest.fn() + const TestUseCalibratePipetteOffset = () => { const [_startCalibration, _CalWizardComponent] = useCalibratePipetteOffset( robotName, - mountString + { + mount: mountString, + shouldPerformTipLength: false, + hasCalibrationBlock: false, + tipRackDefinition: null, + }, + onComplete ) React.useEffect(() => { startCalibration = _startCalibration @@ -69,7 +74,12 @@ describe('useCalibratePipetteOffset hook', () => { ...Sessions.ensureSession( robotName, Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, - { mount: mountString } + { + mount: mountString, + shouldPerformTipLength: false, + hasCalibrationBlock: false, + tipRackDefinition: null, + } ), meta: { requestId: expect.any(String) }, }) @@ -85,12 +95,9 @@ describe('useCalibratePipetteOffset hook', () => { currentStep: Sessions.PIP_OFFSET_STEP_CALIBRATION_COMPLETE, }, } - const { store, wrapper } = mountWithStore( - , - { - initialState: { robotApi: {} }, - } - ) + const { wrapper } = mountWithStore(, { + initialState: { robotApi: {} }, + }) mockGetRobotSessionOfType.mockReturnValue(mockPipOffsetCalSession) mockGetRequestById.mockReturnValue({ status: RobotApi.SUCCESS, @@ -105,8 +112,11 @@ describe('useCalibratePipetteOffset hook', () => { wrapper.setProps({}) expect(CalWizardComponent).not.toBe(null) - wrapper.find('button[children="exit"]').invoke('onClick')() + wrapper + .find('button[title="Return tip to tip rack and exit"]') + .invoke('onClick')() wrapper.setProps({}) expect(CalWizardComponent).toBe(null) + expect(onComplete).toHaveBeenCalled() }) }) diff --git a/app/src/components/CalibratePipetteOffset/useCalibratePipetteOffset.js b/app/src/components/CalibratePipetteOffset/useCalibratePipetteOffset.js index 0083cd5093cc..4881b9244a15 100644 --- a/app/src/components/CalibratePipetteOffset/useCalibratePipetteOffset.js +++ b/app/src/components/CalibratePipetteOffset/useCalibratePipetteOffset.js @@ -1,7 +1,6 @@ // @flow import * as React from 'react' import { useSelector } from 'react-redux' -import { SecondaryBtn, SPACING_2 } from '@opentrons/components' import * as RobotApi from '../../robot-api' import * as Sessions from '../../sessions' @@ -10,8 +9,8 @@ import type { State } from '../../types' import type { SessionCommandString, PipetteOffsetCalibrationSession, + PipetteOffsetCalibrationSessionParams, } from '../../sessions/types' -import type { Mount } from '../../pipettes/types' import type { RequestState } from '../../robot-api/types' import { Portal } from '../portal' @@ -24,7 +23,7 @@ const spinnerCommandBlockList: Array = [ export function useCalibratePipetteOffset( robotName: string, - mount: Mount, + sessionParams: $Shape, onComplete: (() => mixed) | null = null ): [() => void, React.Node | null] { const [showWizard, setShowWizard] = React.useState(false) @@ -74,24 +73,47 @@ export function useCalibratePipetteOffset( : null )?.status === RobotApi.SUCCESS + const closeWizard = React.useCallback(() => { + setShowWizard(false) + onComplete && onComplete() + }, [onComplete]) + React.useEffect(() => { if (shouldOpen) { setShowWizard(true) createRequestId.current = null } if (shouldClose) { - setShowWizard(false) - onComplete && onComplete() + closeWizard() deleteRequestId.current = null } - }, [shouldOpen, shouldClose]) + }, [shouldOpen, shouldClose, closeWizard]) + const { + mount, + shouldPerformTipLength = false, + hasCalibrationBlock = false, + tipRackDefinition = null, + } = sessionParams const handleStartPipOffsetCalSession = () => { + console.log('HANDLE START') + console.table({ + robotName, + mount, + shouldPerformTipLength, + hasCalibrationBlock, + tipRackDefinition, + }) dispatchRequests( Sessions.ensureSession( robotName, Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, - { mount } + { + mount, + shouldPerformTipLength, + hasCalibrationBlock, + tipRackDefinition, + } ) ) } @@ -118,7 +140,7 @@ export function useCalibratePipetteOffset( setShowWizard(false)} + closeWizard={closeWizard} showSpinner={showSpinner} dispatchRequests={dispatchRequests} /> diff --git a/app/src/components/ChangePipette/ConfirmPipette.js b/app/src/components/ChangePipette/ConfirmPipette.js index d3d3bc8262f7..01b7a3f16118 100644 --- a/app/src/components/ChangePipette/ConfirmPipette.js +++ b/app/src/components/ChangePipette/ConfirmPipette.js @@ -2,14 +2,7 @@ import * as React from 'react' import cx from 'classnames' -import { - Icon, - PrimaryBtn, - ModalPage, - SPACING_2, - SPACING_4, -} from '@opentrons/components' -import { PipetteOffsetCalibrationControl } from '../InstrumentSettings/PipetteOffsetCalibrationControl' +import { Icon, PrimaryBtn, ModalPage, SPACING_2 } from '@opentrons/components' import { getDiagramsSrc } from './InstructionStep' import { CheckPipettesButton } from './CheckPipettesButton' import styles from './styles.css' @@ -21,7 +14,6 @@ import type { } from '@opentrons/shared-data' import type { Mount } from '../../pipettes/types' import type { PipetteOffsetCalibration } from '../../calibration/types' -import { CalibratePipetteOffset } from '../CalibratePipetteOffset' const EXIT_BUTTON_MESSAGE = 'exit pipette setup' const EXIT_BUTTON_MESSAGE_WRONG = 'keep pipette and exit setup' @@ -54,9 +46,6 @@ export function ConfirmPipette(props: Props): React.Node { actualPipette, actualPipetteOffset, back, - robotName, - mount, - startPipetteOffsetCalibration, } = props return ( diff --git a/app/src/components/ChangePipette/index.js b/app/src/components/ChangePipette/index.js index a7f70524e60c..add428ec0e88 100644 --- a/app/src/components/ChangePipette/index.js +++ b/app/src/components/ChangePipette/index.js @@ -1,10 +1,13 @@ // @flow import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' -import last from 'lodash/last' import { getPipetteNameSpecs, shouldLevel } from '@opentrons/shared-data' -import { useDispatchApiRequest, getRequestById, PENDING } from '../../robot-api' +import { + useDispatchApiRequests, + getRequestById, + SUCCESS, +} from '../../robot-api' import { getCalibrationForPipette } from '../../calibration' import { getAttachedPipettes } from '../../pipettes' import { @@ -13,8 +16,10 @@ import { getMovementStatus, HOMING, MOVING, + ROBOT, PIPETTE, CHANGE_PIPETTE, + HOME, } from '../../robot-controls' import { useCalibratePipetteOffset } from '../CalibratePipetteOffset' @@ -53,7 +58,16 @@ const MOUNT = 'mount' export function ChangePipette(props: Props): React.Node { const { robotName, mount, closeModal } = props const dispatch = useDispatch() - const [dispatchApiRequest, requestIds] = useDispatchApiRequest() + const homePipRequestId = React.useRef(null) + const [dispatchApiRequests] = useDispatchApiRequests(dispatchedAction => { + if ( + dispatchedAction.type === HOME && + dispatchedAction.payload.target === PIPETTE + ) { + // track final home pipette request, its success closes modal + homePipRequestId.current = dispatchedAction.meta.requestId + } + }) const [wizardStep, setWizardStep] = React.useState(CLEAR_DECK) const [wantedName, setWantedName] = React.useState(null) const [confirmExit, setConfirmExit] = React.useState(false) @@ -72,25 +86,28 @@ export function ChangePipette(props: Props): React.Node { return getMovementStatus(state, robotName) }) - const homeRequest = useSelector((state: State) => { - return getRequestById(state, last(requestIds)) - })?.status + const homePipSuccess = + useSelector((state: State) => { + return homePipRequestId.current + ? getRequestById(state, homePipRequestId.current) + : null + })?.status === SUCCESS React.useEffect(() => { - if (homeRequest && homeRequest !== PENDING) { + if (homePipSuccess) { closeModal() } - }, [homeRequest, closeModal]) + }, [homePipSuccess, closeModal]) - const homeAndExit = React.useCallback( - () => dispatchApiRequest(home(robotName, PIPETTE, mount)), - [dispatchApiRequest, robotName, mount] + const homePipAndExit = React.useCallback( + () => dispatchApiRequests(home(robotName, PIPETTE, mount)), + [dispatchApiRequests, robotName, mount] ) const [ startPipetteOffsetCalibration, PipetteOffsetCalibrationWizard, - ] = useCalibratePipetteOffset(robotName, mount, closeModal) + ] = useCalibratePipetteOffset(robotName, { mount }, closeModal) const baseProps = { title: PIPETTE_SETUP, @@ -139,7 +156,7 @@ export function ChangePipette(props: Props): React.Node { {confirmExit && ( setConfirmExit(false)} - exit={homeAndExit} + exit={homePipAndExit} /> )} { + // home before cal flow to account for skips when attaching pipette + setWizardStep(CALIBRATE_PIPETTE) + dispatchApiRequests(home(robotName, ROBOT)) + startPipetteOffsetCalibration() + } + if (success && wantedPipette && shouldLevel(wantedPipette)) { return ( setWizardStep(INSTRUCTIONS), - exit: homeAndExit, + exit: homePipAndExit, actualPipetteOffset: actualPipetteOffset, - startPipetteOffsetCalibration: () => { - startPipetteOffsetCalibration() - setWizardStep(CALIBRATE_PIPETTE) - }, + startPipetteOffsetCalibration: launchPOC, }} /> ) @@ -192,12 +213,9 @@ export function ChangePipette(props: Props): React.Node { setWizardStep(INSTRUCTIONS) }, back: () => setWizardStep(INSTRUCTIONS), - exit: homeAndExit, + exit: homePipAndExit, actualPipetteOffset: actualPipetteOffset, - startPipetteOffsetCalibration: () => { - startPipetteOffsetCalibration() - setWizardStep(CALIBRATE_PIPETTE) - }, + startPipetteOffsetCalibration: launchPOC, }} /> ) diff --git a/app/src/components/InstrumentSettings/PipetteInfo.js b/app/src/components/InstrumentSettings/PipetteInfo.js index 4f042c70c2bd..91f6d432a848 100644 --- a/app/src/components/InstrumentSettings/PipetteInfo.js +++ b/app/src/components/InstrumentSettings/PipetteInfo.js @@ -68,7 +68,7 @@ export function PipetteInfo(props: PipetteInfoProps): React.Node { const [ startPipetteOffsetCalibration, PipetteOffsetCalibrationWizard, - ] = useCalibratePipetteOffset(robotName, mount) + ] = useCalibratePipetteOffset(robotName, { mount }) const pipImage = ( = [ - Sessions.sharedCalCommands.JOG, -] - -const BUTTON_TEXT = 'Calibrate offset' - -export function PipetteOffsetCalibrationControl(props: Props): React.Node { - const { robotName, mount } = props - - const [showWizard, setShowWizard] = React.useState(false) - - const trackedRequestId = React.useRef(null) - const deleteRequestId = React.useRef(null) - const createRequestId = React.useRef(null) - - const [dispatchRequests] = RobotApi.useDispatchApiRequests( - dispatchedAction => { - if (dispatchedAction.type === Sessions.ENSURE_SESSION) { - createRequestId.current = dispatchedAction.meta.requestId - } else if ( - dispatchedAction.type === Sessions.DELETE_SESSION && - pipOffsetCalSession?.id === dispatchedAction.payload.sessionId - ) { - deleteRequestId.current = dispatchedAction.meta.requestId - } else if ( - dispatchedAction.type !== Sessions.CREATE_SESSION_COMMAND || - !spinnerCommandBlockList.includes( - dispatchedAction.payload.command.command - ) - ) { - trackedRequestId.current = dispatchedAction.meta.requestId - } - } - ) - - const showSpinner = - useSelector(state => - trackedRequestId.current - ? RobotApi.getRequestById(state, trackedRequestId.current) - : null - )?.status === RobotApi.PENDING - - const shouldClose = - useSelector(state => - deleteRequestId.current - ? RobotApi.getRequestById(state, deleteRequestId.current) - : null - )?.status === RobotApi.SUCCESS - - const shouldOpen = - useSelector((state: State) => - createRequestId.current - ? RobotApi.getRequestById(state, createRequestId.current) - : null - )?.status === RobotApi.SUCCESS - - React.useEffect(() => { - if (shouldOpen) { - setShowWizard(true) - createRequestId.current = null - } - if (shouldClose) { - setShowWizard(false) - deleteRequestId.current = null - } - }, [shouldOpen, shouldClose]) - - const hasCalibrationBlock = false - const shouldPerformTipLength = false - const tipRackDefinition = null - const handleStartPipOffsetCalSession = () => { - dispatchRequests( - Sessions.ensureSession( - robotName, - Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, - { - mount, - shouldPerformTipLength, - hasCalibrationBlock, - tipRackDefinition, - } - ) - ) - } - - const pipOffsetCalSession = useSelector< - State, - PipetteOffsetCalibrationSession | null - >((state: State) => { - const session: Sessions.Session | null = Sessions.getRobotSessionOfType( - state, - robotName, - Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION - ) - return session && - session.sessionType === Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION - ? session - : null - }) - - const [ - startPipetteOffsetCalibration, - PipetteOffsetCalibrationWizard, - ] = useCalibratePipetteOffset() - - return ( - <> - - {BUTTON_TEXT} - - {showWizard && ( - - setShowWizard(false)} - showSpinner={showSpinner} - dispatchRequests={dispatchRequests} - /> - - )} - - ) -}