From c8fc9f2714c27893c8a0db13e427d776032754f1 Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Tue, 5 Jan 2021 13:53:22 -0500 Subject: [PATCH] feat(app): allow custom tiprack selection in deck cal & pipette offset cal (#7155) * feat(app): allow custom tiprack selection in deck cal & pipette offset cal closes #7087 * link choose tiprack screen in intro * render selected tiprack in intro screen * add uri param to tip len cal and new selector * add recalibrate pip offset intent * fix flow & test errors * reformat intro screen continue button * add tests * fix css lint error * add cancel button * conditionally show dropdown labels * warn about override tip length cal in tlc * show previously chosen tiprack as default * use pip offset tiprack hash to check tip length data * match selector-class-pattern * fix broken tests --- app/src/calibration/api-types.js | 1 + app/src/calibration/tip-length/selectors.js | 19 ++ .../__tests__/CalibratePipetteOffset.test.js | 4 +- .../CalibratePipetteOffset/index.js | 1 + .../useCalibratePipetteOffset.js | 8 +- .../CalibrationPanels/ChooseTipRack.js | 308 ++++++++++++++++++ .../CalibrationPanels/ChosenTipRackRender.js | 101 ++++++ .../CalibrationPanels/Introduction.js | 155 +++++++-- .../__tests__/ChooseTipRack.test.js | 115 +++++++ .../__tests__/Introduction.test.js | 40 ++- .../components/CalibrationPanels/constants.js | 8 +- .../components/CalibrationPanels/styles.css | 7 + app/src/components/CalibrationPanels/types.js | 9 +- app/src/components/ChangePipette/index.js | 4 +- .../PipetteCalibrationInfo.js | 6 +- .../__tests__/PipetteInfo.test.js | 8 +- app/src/custom-labware/__fixtures__/index.js | 23 ++ app/src/custom-labware/selectors.js | 8 + 18 files changed, 775 insertions(+), 50 deletions(-) create mode 100644 app/src/components/CalibrationPanels/ChooseTipRack.js create mode 100644 app/src/components/CalibrationPanels/ChosenTipRackRender.js create mode 100644 app/src/components/CalibrationPanels/__tests__/ChooseTipRack.test.js diff --git a/app/src/calibration/api-types.js b/app/src/calibration/api-types.js index 86ef9fc2fe2..f45f387e6de 100644 --- a/app/src/calibration/api-types.js +++ b/app/src/calibration/api-types.js @@ -130,6 +130,7 @@ export type TipLengthCalibration = {| source: CalibrationSource, status: IndividualCalibrationStatus, id: string, + uri?: string | null, |} export type AllTipLengthCalibrations = {| diff --git a/app/src/calibration/tip-length/selectors.js b/app/src/calibration/tip-length/selectors.js index bc49bad226b..57f4f944422 100644 --- a/app/src/calibration/tip-length/selectors.js +++ b/app/src/calibration/tip-length/selectors.js @@ -1,4 +1,5 @@ // @flow +import { createSelector } from 'reselect' import { head } from 'lodash' import type { State } from '../../types' @@ -16,6 +17,24 @@ export const getTipLengthCalibrations: ( return calibrations } +export const getTipLengthForPipetteAndTiprackByUri: ( + state: State, + robotName: string | null, + pipetteSerial: string, + tiprackUri: string +) => TipLengthCalibration | null = createSelector( + getTipLengthCalibrations, + (allCalibrations, pipetteSerial, tipRackUri) => { + return ( + head( + allCalibrations.filter( + cal => cal.pipette === pipetteSerial && cal.uri === tipRackUri + ) + ) || null + ) + } +) + export const filterTipLengthForPipetteAndTiprack: ( allCalibrations: Array<TipLengthCalibration>, pipetteSerial: string | null, diff --git a/app/src/components/CalibratePipetteOffset/__tests__/CalibratePipetteOffset.test.js b/app/src/components/CalibratePipetteOffset/__tests__/CalibratePipetteOffset.test.js index 625c2af087d..5ef8d3d1ede 100644 --- a/app/src/components/CalibratePipetteOffset/__tests__/CalibratePipetteOffset.test.js +++ b/app/src/components/CalibratePipetteOffset/__tests__/CalibratePipetteOffset.test.js @@ -18,7 +18,7 @@ import { SaveZPoint, SaveXYPoint, CompleteConfirmation, - INTENT_PIPETTE_OFFSET, + INTENT_CALIBRATE_PIPETTE_OFFSET, } from '../../CalibrationPanels' import type { PipetteOffsetCalibrationStep } from '../../../sessions/types' @@ -99,7 +99,7 @@ describe('CalibratePipetteOffset', () => { dispatchRequests={dispatchRequests} showSpinner={showSpinner} isJogging={isJogging} - intent={INTENT_PIPETTE_OFFSET} + intent={INTENT_CALIBRATE_PIPETTE_OFFSET} />, { wrappingComponent: Provider, diff --git a/app/src/components/CalibratePipetteOffset/index.js b/app/src/components/CalibratePipetteOffset/index.js index 04eb420c216..495f9f0f053 100644 --- a/app/src/components/CalibratePipetteOffset/index.js +++ b/app/src/components/CalibratePipetteOffset/index.js @@ -184,6 +184,7 @@ export function CalibratePipetteOffset( sessionType={session.sessionType} shouldPerformTipLength={shouldPerformTipLength} intent={intent} + robotName={robotName} /> </ModalPage> {showConfirmExit && ( diff --git a/app/src/components/CalibratePipetteOffset/useCalibratePipetteOffset.js b/app/src/components/CalibratePipetteOffset/useCalibratePipetteOffset.js index fc2a2cc55dc..90449afe61d 100644 --- a/app/src/components/CalibratePipetteOffset/useCalibratePipetteOffset.js +++ b/app/src/components/CalibratePipetteOffset/useCalibratePipetteOffset.js @@ -19,7 +19,7 @@ import type { PipetteOffsetIntent } from '../CalibrationPanels/types' import { Portal } from '../portal' import { CalibratePipetteOffset } from '../CalibratePipetteOffset' -import { INTENT_PIPETTE_OFFSET } from '../CalibrationPanels' +import { INTENT_CALIBRATE_PIPETTE_OFFSET } from '../CalibrationPanels' import { pipetteOffsetCalibrationStarted } from '../../analytics' // pipette calibration commands for which the full page spinner should not appear @@ -121,7 +121,7 @@ export function useCalibratePipetteOffset( }, [shouldClose, onComplete]) const [intent, setIntent] = React.useState<PipetteOffsetIntent>( - INTENT_PIPETTE_OFFSET + INTENT_CALIBRATE_PIPETTE_OFFSET ) const { @@ -133,7 +133,7 @@ export function useCalibratePipetteOffset( const handleStartPipOffsetCalSession: Invoker = (props = {}) => { const { overrideParams = ({}: $Shape<PipetteOffsetCalibrationSessionParams>), - withIntent = INTENT_PIPETTE_OFFSET, + withIntent = INTENT_CALIBRATE_PIPETTE_OFFSET, } = props setIntent(withIntent) dispatchRequests( @@ -173,7 +173,7 @@ export function useCalibratePipetteOffset( <SpinnerModalPage titleBar={{ title: - intent === INTENT_PIPETTE_OFFSET + intent === INTENT_CALIBRATE_PIPETTE_OFFSET ? PIPETTE_OFFSET_TITLE : TIP_LENGTH_TITLE, back: { diff --git a/app/src/components/CalibrationPanels/ChooseTipRack.js b/app/src/components/CalibrationPanels/ChooseTipRack.js new file mode 100644 index 00000000000..450f4e6595c --- /dev/null +++ b/app/src/components/CalibrationPanels/ChooseTipRack.js @@ -0,0 +1,308 @@ +// @flow +import * as React from 'react' +import { useSelector } from 'react-redux' +import { head } from 'lodash' +import isEqual from 'lodash/isEqual' + +import { + AlertItem, + ALIGN_FLEX_START, + BORDER_SOLID_MEDIUM, + Box, + DIRECTION_COLUMN, + Flex, + FONT_HEADER_DARK, + FONT_SIZE_BODY_2, + JUSTIFY_SPACE_BETWEEN, + JUSTIFY_CENTER, + POSITION_RELATIVE, + PrimaryBtn, + Select, + SPACING_1, + SPACING_2, + SPACING_3, + SPACING_4, + Text, + TEXT_TRANSFORM_UPPERCASE, + TEXT_TRANSFORM_CAPITALIZE, + FONT_WEIGHT_SEMIBOLD, + ALIGN_CENTER, + SecondaryBtn, +} from '@opentrons/components' +import * as Sessions from '../../sessions' +import { NeedHelpLink } from './NeedHelpLink' +import { ChosenTipRackRender } from './ChosenTipRackRender' +import { getCustomTipRackDefinitions } from '../../custom-labware' +import { getAttachedPipettes } from '../../pipettes' +import { + getCalibrationForPipette, + getTipLengthCalibrations, + getTipLengthForPipetteAndTiprack, +} from '../../calibration/' +import { getLabwareDefURI } from '@opentrons/shared-data' +import { findLabwareDefWithCustom } from '../../findLabware' +import styles from './styles.css' + +import type { TipRackMap } from './ChosenTipRackRender' +import type { SessionType, CalibrationLabware } from '../../sessions/types' +import type { State } from '../../types' +import type { SelectOption, SelectOptionOrGroup } from '@opentrons/components' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +const HEADER = 'choose tip rack' +const INTRO = 'Choose what tip rack you would like to use to calibrate your' +const PIP_OFFSET_INTRO_FRAGMENT = 'Pipette Offset' +const DECK_CAL_INTRO_FRAGMENT = 'Deck' + +const PROMPT = + 'Want to use a tip rack that is not listed here? Go to More > Custom Labware to add labware.' + +const SELECT_TIP_RACK = 'select tip rack' +const ALERT_TEXT = + 'Opentrons tip racks are strongly recommended. Accuracy cannot be guaranteed with other tip racks.' + +const OPENTRONS_LABEL = 'opentrons' +const CUSTOM_LABEL = 'custom' +const USE_THIS_TIP_RACK = 'use this tip rack' + +const introContentByType: SessionType => string = sessionType => { + switch (sessionType) { + case Sessions.SESSION_TYPE_DECK_CALIBRATION: + return `${INTRO} ${DECK_CAL_INTRO_FRAGMENT}.` + case Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION: + return `${INTRO} ${PIP_OFFSET_INTRO_FRAGMENT}.` + default: + return 'This panel is shown in error' + } +} + +function getLabwareDefinitionFromUri( + uri: string, + customTipRacks: Array<LabwareDefinition2> +): LabwareDefinition2 | null { + const [namespace, loadName, version] = uri.split('/') + const labwareDef = findLabwareDefWithCustom( + namespace, + loadName, + version, + customTipRacks + ) + return labwareDef +} + +function formatOptionsFromLabwareDef(lw: LabwareDefinition2): SelectOption { + return { + value: getLabwareDefURI(lw), + label: lw.metadata.displayName, + } +} + +type ChooseTipRackProps = {| + tipRack: CalibrationLabware, + mount: string, + sessionType: SessionType, + chosenTipRack: LabwareDefinition2 | null, + handleChosenTipRack: (arg: LabwareDefinition2 | null) => mixed, + closeModal: () => mixed, + robotName?: string | null, +|} + +export function ChooseTipRack(props: ChooseTipRackProps): React.Node { + const { + tipRack, + mount, + sessionType, + chosenTipRack, + handleChosenTipRack, + closeModal, + robotName, + } = props + + const pipSerial = useSelector( + (state: State) => + robotName && getAttachedPipettes(state, robotName)[mount].id + ) + + const pipetteOffsetCal = useSelector((state: State) => + robotName && pipSerial + ? getCalibrationForPipette(state, robotName, pipSerial, mount) + : null + ) + const tipLengthCal = useSelector((state: State) => + robotName && pipSerial && pipetteOffsetCal + ? getTipLengthForPipetteAndTiprack( + state, + robotName, + pipSerial, + pipetteOffsetCal?.tiprack + ) + : null + ) + const allTipLengthCal = useSelector((state: State) => + robotName ? getTipLengthCalibrations(state, robotName) : [] + ) + const customTipRacks = useSelector(getCustomTipRackDefinitions) + + const opentronsTipRacks: Array<LabwareDefinition2> = [ + 'opentrons/opentrons_96_tiprack_10ul/1', + 'opentrons/opentrons_96_tiprack_20ul/1', + 'opentrons/opentrons_96_tiprack_300ul/1', + ].reduce((acc, tr) => { + const def = getLabwareDefinitionFromUri(tr, customTipRacks) + if (def) { + acc.push(def) + } + return acc + }, []) // TODO: Actually get the default tipracks from user flow + + const allTipRackDefs = opentronsTipRacks.concat(customTipRacks) + const tipRackByUriMap: TipRackMap = allTipRackDefs.reduce((obj, lw) => { + if (lw) { + obj[getLabwareDefURI(lw)] = { + definition: lw, + calibration: + head( + allTipLengthCal.filter( + cal => + cal.pipette === pipSerial && cal.uri === getLabwareDefURI(lw) + ) + ) || + // Old tip length data don't have tiprack uri info, so we are using the + // tiprack hash in pipette offset to check against tip length cal for + // backward compatability purposes + (pipetteOffsetCal && + tipLengthCal && + pipetteOffsetCal.tiprackUri === getLabwareDefURI(lw) + ? tipLengthCal + : null), + } + } + return obj + }, {}) + + const opentronsTipRacksOptions: Array<SelectOption> = opentronsTipRacks.map( + lw => formatOptionsFromLabwareDef(lw) + ) + const customTipRacksOptions: Array<SelectOption> = customTipRacks.map(lw => + formatOptionsFromLabwareDef(lw) + ) + + const groupOptions: Array<SelectOptionOrGroup> = + customTipRacks.length > 0 + ? [ + { + label: OPENTRONS_LABEL, + options: opentronsTipRacksOptions, + }, + { + label: CUSTOM_LABEL, + options: customTipRacksOptions, + }, + ] + : [...opentronsTipRacksOptions] + + const [selectedValue, setSelectedValue] = React.useState<SelectOption>( + chosenTipRack + ? formatOptionsFromLabwareDef(chosenTipRack) + : formatOptionsFromLabwareDef(tipRack.definition) + ) + + const handleValueChange = (selected: SelectOption | null, _) => { + selected && setSelectedValue(selected) + } + const handleUseTipRack = () => { + const selectedTipRack = tipRackByUriMap[selectedValue.value] + if (!isEqual(chosenTipRack, selectedTipRack.definition)) { + handleChosenTipRack(selectedTipRack.definition) + } + closeModal() + } + const introText = introContentByType(sessionType) + return ( + <Flex + key={'chooseTipRack'} + marginTop={SPACING_2} + marginBottom={SPACING_3} + flexDirection={DIRECTION_COLUMN} + alignItems={ALIGN_FLEX_START} + position={POSITION_RELATIVE} + fontSize={FONT_SIZE_BODY_2} + width="100%" + > + <Flex width="100%" justifyContent={JUSTIFY_SPACE_BETWEEN}> + <Text + css={FONT_HEADER_DARK} + marginBottom={SPACING_3} + textTransform={TEXT_TRANSFORM_UPPERCASE} + > + {HEADER} + </Text> + <NeedHelpLink /> + </Flex> + <Box marginBottom={SPACING_3}> + <Text marginBottom={SPACING_3}>{introText}</Text> + <Text>{PROMPT}</Text> + </Box> + <Flex marginBottom={SPACING_4}> + <AlertItem type="warning" title={ALERT_TEXT} /> + </Flex> + <Flex + width="80%" + marginBottom={SPACING_4} + justifyContent={JUSTIFY_CENTER} + flexDirection={DIRECTION_COLUMN} + alignSelf={ALIGN_CENTER} + > + <Flex + height="16rem" + border={BORDER_SOLID_MEDIUM} + paddingTop={SPACING_4} + justifyContent={JUSTIFY_SPACE_BETWEEN} + marginBottom={SPACING_2} + > + <Box width="55%" paddingLeft={SPACING_4}> + <Text + textTransform={TEXT_TRANSFORM_CAPITALIZE} + fontWeight={FONT_WEIGHT_SEMIBOLD} + marginBottom={SPACING_1} + > + {SELECT_TIP_RACK} + </Text> + <Select + className={styles.select_tiprack_menu} + options={groupOptions} + onChange={handleValueChange} + value={selectedValue} + /> + </Box> + <Box width="45%" height="100%"> + <ChosenTipRackRender + showCalibrationText={ + sessionType === Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION + } + selectedValue={selectedValue} + tipRackByUriMap={tipRackByUriMap} + /> + </Box> + </Flex> + </Flex> + <Flex width="100%" justifyContent={JUSTIFY_CENTER}> + <SecondaryBtn + data-test="useThisTipRackButton" + width="25%" + marginRight="1rem" + onClick={() => closeModal()} + > + Cancel + </SecondaryBtn> + <PrimaryBtn + data-test="useThisTipRackButton" + width="48%" + onClick={handleUseTipRack} + > + {USE_THIS_TIP_RACK} + </PrimaryBtn> + </Flex> + </Flex> + ) +} diff --git a/app/src/components/CalibrationPanels/ChosenTipRackRender.js b/app/src/components/CalibrationPanels/ChosenTipRackRender.js new file mode 100644 index 00000000000..68e3ed3a14b --- /dev/null +++ b/app/src/components/CalibrationPanels/ChosenTipRackRender.js @@ -0,0 +1,101 @@ +// @flow +import * as React from 'react' +import { css } from 'styled-components' + +import { + Box, + Flex, + Text, + ALIGN_CENTER, + C_MED_DARK_GRAY, + DIRECTION_COLUMN, + FONT_SIZE_BODY_1, + FONT_SIZE_BODY_2, + FONT_STYLE_ITALIC, + JUSTIFY_CENTER, + SPACING_2, + SPACING_3, + TEXT_ALIGN_CENTER, +} from '@opentrons/components' +import { labwareImages } from './labwareImages' +import { formatLastModified } from './utils' + +import type { SelectOption } from '@opentrons/components' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { TipLengthCalibration } from '../../calibration/api-types' + +const TIP_LENGTH_CALIBRATED_PROMPT = 'Calibrated on' +const OVERRIDE_TIP_LENGTH_CALIBRATED_PROMPT = + 'Choosing this tiprack will override previous Tip Length Calibration data.' +const TIP_LENGTH_UNCALIBRATED_PROMPT = + 'Not yet calibrated. You will calibrate this tip length before proceeding to Pipette Offset Calibration.' + +type TipRackInfo = {| + definition: LabwareDefinition2, + calibration: TipLengthCalibration | null, +|} + +export type TipRackMap = $Shape<{| + [uri: string]: TipRackInfo, +|}> + +export type ChosenTipRackRenderProps = {| + showCalibrationText: boolean, + selectedValue: SelectOption, + tipRackByUriMap: TipRackMap, +|} + +export function ChosenTipRackRender( + props: ChosenTipRackRenderProps +): React.Node { + const { showCalibrationText, selectedValue, tipRackByUriMap } = props + const loadName = selectedValue.value.split('/')[1] + const displayName = selectedValue.label + const calibrationData = tipRackByUriMap[selectedValue.value].calibration + + const imageSrc = + loadName in labwareImages + ? labwareImages[loadName] + : labwareImages['generic_custom_tiprack'] + return ( + <Flex + height="100%" + flexDirection={DIRECTION_COLUMN} + alignItems={ALIGN_CENTER} + justifyContent={JUSTIFY_CENTER} + paddingRight={SPACING_2} + paddingBottom={SPACING_3} + fontSize={FONT_SIZE_BODY_2} + > + <img + css={css` + max-width: 8rem; + max-height: 6rem; + flex: 0 1 5rem; + display: block; + margin-bottom: 1rem; + `} + src={imageSrc} + /> + <Box> + <Text textAlign={TEXT_ALIGN_CENTER} marginBottom={SPACING_2}> + {displayName} + </Text> + {showCalibrationText && ( + <Text + color={C_MED_DARK_GRAY} + fontSize={FONT_SIZE_BODY_1} + fontStyle={FONT_STYLE_ITALIC} + textAlign={TEXT_ALIGN_CENTER} + > + {calibrationData + ? `${TIP_LENGTH_CALIBRATED_PROMPT} ${formatLastModified( + calibrationData.lastModified + )}. ${OVERRIDE_TIP_LENGTH_CALIBRATED_PROMPT}` + : TIP_LENGTH_UNCALIBRATED_PROMPT} + </Text> + )} + </Box> + </Flex> + ) +} diff --git a/app/src/components/CalibrationPanels/Introduction.js b/app/src/components/CalibrationPanels/Introduction.js index f69503df190..c7dd263ac87 100644 --- a/app/src/components/CalibrationPanels/Introduction.js +++ b/app/src/components/CalibrationPanels/Introduction.js @@ -27,17 +27,21 @@ import { SPACING_2, SPACING_3, TEXT_TRANSFORM_UPPERCASE, + SecondaryBtn, } from '@opentrons/components' import * as Sessions from '../../sessions' import { labwareImages } from './labwareImages' import { NeedHelpLink } from './NeedHelpLink' +import { ChooseTipRack } from './ChooseTipRack' +import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SessionType } from '../../sessions/types' import type { CalibrationPanelProps, Intent } from './types' import { INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL, INTENT_TIP_LENGTH_IN_PROTOCOL, - INTENT_PIPETTE_OFFSET, + INTENT_CALIBRATE_PIPETTE_OFFSET, + INTENT_RECALIBRATE_PIPETTE_OFFSET, TRASH_BIN_LOAD_NAME, } from './constants' @@ -70,6 +74,7 @@ const TIP_LENGTH_CAL_EXPLANATION_FRAGMENT = const TIP_LENGTH_INVALIDATES_PIPETTE_OFFSET = 'This tip was used to calibrate this pipette’s offset. Recalibrating this tip’s length will invalidate this pipette’s offset. If you recalibrate this tip length, you will need to recalibrate this pipette offset afterwards.' +const CHOOSE_TIP_RACK_BUTTON_TEXT = 'Use a different tip rack' const START = 'start' const PIP_AND_TIP_CAL_HEADER = 'tip length and pipette offset calibration' const LABWARE_REQS = 'You will need:' @@ -96,6 +101,7 @@ type PanelContents = {| invalidationText: string | null, bodyContentFragments: Array<BodySpec>, outcomeText: string | null, + chooseTipRackButtonText: string | null, continueButtonText: string, noteBody: BodySpec, |} @@ -146,6 +152,7 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( }, ], outcomeText: null, + chooseTipRackButtonText: CHOOSE_TIP_RACK_BUTTON_TEXT, continueButtonText: `${START} ${DECK_CAL_HEADER}`, noteBody: { preFragment: IT_IS, @@ -165,6 +172,7 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( }, ], outcomeText: null, + chooseTipRackButtonText: null, continueButtonText: `${START} ${TIP_LENGTH_CAL_HEADER}`, noteBody: { preFragment: IT_IS, @@ -195,6 +203,7 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( }, ], outcomeText: null, + chooseTipRackButtonText: null, continueButtonText: `${START} ${TIP_LENGTH_CAL_HEADER}`, noteBody: { preFragment: IT_IS, @@ -218,6 +227,7 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( postFragment: PIP_OFFSET_CAL_EXPLANATION_FRAGMENT, }, ], + chooseTipRackButtonText: CHOOSE_TIP_RACK_BUTTON_TEXT, outcomeText: null, continueButtonText: `${START} ${TIP_LENGTH_CAL_HEADER}`, noteBody: { @@ -226,7 +236,7 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( postFragment: NOTE_BODY_OUTSIDE_PROTOCOL, }, } - case INTENT_PIPETTE_OFFSET: + case INTENT_CALIBRATE_PIPETTE_OFFSET: return { headerText: PIP_AND_TIP_CAL_HEADER, invalidationText: PIP_OFFSET_REQUIRES_TIP_LENGTH, @@ -243,6 +253,32 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( }, ], outcomeText: null, + chooseTipRackButtonText: CHOOSE_TIP_RACK_BUTTON_TEXT, + continueButtonText: `${START} ${TIP_LENGTH_CAL_HEADER}`, + noteBody: { + preFragment: IT_IS, + boldFragment: EXTREMELY, + postFragment: NOTE_BODY_OUTSIDE_PROTOCOL, + }, + } + case INTENT_RECALIBRATE_PIPETTE_OFFSET: + return { + headerText: PIP_AND_TIP_CAL_HEADER, + invalidationText: PIP_OFFSET_REQUIRES_TIP_LENGTH, + bodyContentFragments: [ + { + preFragment: null, + boldFragment: TIP_LENGTH_CAL_NAME_FRAGMENT, + postFragment: TIP_LENGTH_CAL_EXPLANATION_FRAGMENT, + }, + { + preFragment: PIP_OFFSET_CAL_INTRO_FRAGMENT, + boldFragment: PIP_OFFSET_CAL_NAME_FRAGMENT, + postFragment: PIP_OFFSET_CAL_EXPLANATION_FRAGMENT, + }, + ], + outcomeText: null, + chooseTipRackButtonText: CHOOSE_TIP_RACK_BUTTON_TEXT, continueButtonText: `${START} ${TIP_LENGTH_CAL_HEADER}`, noteBody: { preFragment: IT_IS, @@ -262,6 +298,7 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( }, ], outcomeText: null, + chooseTipRackButtonText: CHOOSE_TIP_RACK_BUTTON_TEXT, continueButtonText: `${START} ${PIP_OFFSET_CAL_HEADER}`, noteBody: { preFragment: IT_IS, @@ -271,23 +308,47 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( } } } else { - return { - headerText: PIP_OFFSET_CAL_HEADER, - invalidationText: null, - bodyContentFragments: [ - { - preFragment: PIP_OFFSET_CAL_INTRO_FRAGMENT, - boldFragment: PIP_OFFSET_CAL_NAME_FRAGMENT, - postFragment: PIP_OFFSET_CAL_EXPLANATION_FRAGMENT, - }, - ], - outcomeText: null, - continueButtonText: `${START} ${PIP_OFFSET_CAL_HEADER}`, - noteBody: { - preFragment: IT_IS, - boldFragment: EXTREMELY, - postFragment: NOTE_BODY_OUTSIDE_PROTOCOL, - }, + switch (intent) { + case INTENT_RECALIBRATE_PIPETTE_OFFSET: + return { + headerText: PIP_OFFSET_CAL_HEADER, + invalidationText: null, + bodyContentFragments: [ + { + preFragment: PIP_OFFSET_CAL_INTRO_FRAGMENT, + boldFragment: PIP_OFFSET_CAL_NAME_FRAGMENT, + postFragment: PIP_OFFSET_CAL_EXPLANATION_FRAGMENT, + }, + ], + outcomeText: null, + chooseTipRackButtonText: null, + continueButtonText: `${START} ${PIP_OFFSET_CAL_HEADER}`, + noteBody: { + preFragment: IT_IS, + boldFragment: EXTREMELY, + postFragment: NOTE_BODY_OUTSIDE_PROTOCOL, + }, + } + default: + return { + headerText: PIP_OFFSET_CAL_HEADER, + invalidationText: null, + bodyContentFragments: [ + { + preFragment: PIP_OFFSET_CAL_INTRO_FRAGMENT, + boldFragment: PIP_OFFSET_CAL_NAME_FRAGMENT, + postFragment: PIP_OFFSET_CAL_EXPLANATION_FRAGMENT, + }, + ], + outcomeText: null, + chooseTipRackButtonText: null, + continueButtonText: `${START} ${PIP_OFFSET_CAL_HEADER}`, + noteBody: { + preFragment: IT_IS, + boldFragment: EXTREMELY, + postFragment: NOTE_BODY_OUTSIDE_PROTOCOL, + }, + } } } case Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK: @@ -302,6 +363,7 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( }, ], outcomeText: HEALTH_CHECK_PROMPT_FRAGMENT, + chooseTipRackButtonText: null, continueButtonText: `${START} ${HEALTH_CHECK_HEADER}`, noteBody: { preFragment: null, @@ -315,6 +377,7 @@ const contentsByParams: (SessionType, ?boolean, ?Intent) => PanelContents = ( invalidationText: 'This panel is shown in error', bodyContentFragments: [], continueButtonText: 'Error', + chooseTipRackButtonText: null, outcomeText: null, noteBody: { preFragment: null, boldFragment: null, postFragment: null }, } @@ -332,12 +395,22 @@ export function Introduction(props: CalibrationPanelProps): React.Node { instruments, } = props + const [showChooseTipRack, setShowChooseTipRack] = React.useState(false) + const [ + chosenTipRack, + setChosenTipRack, + ] = React.useState<LabwareDefinition2 | null>(null) + + const handleChosenTipRack = (value: LabwareDefinition2 | null) => { + value && setChosenTipRack(value) + } const isExtendedPipOffset = sessionType === Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION && shouldPerformTipLength const uniqueTipRacks = new Set( instruments?.map(instr => instr.tipRackLoadName) ) + const proceed = () => sendCommands({ command: Sessions.sharedCalCommands.LOAD_LABWARE }) @@ -346,12 +419,23 @@ export function Introduction(props: CalibrationPanelProps): React.Node { invalidationText, bodyContentFragments, outcomeText, + chooseTipRackButtonText, continueButtonText, noteBody, } = contentsByParams(sessionType, isExtendedPipOffset, intent) const isKnownTiprack = tipRack.loadName in labwareImages - return ( + return showChooseTipRack ? ( + <ChooseTipRack + tipRack={props.tipRack} + mount={props.mount} + sessionType={props.sessionType} + chosenTipRack={chosenTipRack} + handleChosenTipRack={handleChosenTipRack} + closeModal={() => setShowChooseTipRack(false)} + robotName={props.robotName} + /> + ) : ( <> <Flex key={'intro'} @@ -400,6 +484,14 @@ export function Introduction(props: CalibrationPanelProps): React.Node { /> ) }) + ) : chosenTipRack ? ( + <RequiredLabwareCard + loadName={chosenTipRack.parameters.loadName} + displayName={chosenTipRack.metadata.displayName} + linkToMeasurements={ + chosenTipRack.parameters.loadName in labwareImages + } + /> ) : ( <RequiredLabwareCard loadName={tipRack.loadName} @@ -440,13 +532,22 @@ export function Introduction(props: CalibrationPanelProps): React.Node { </Box> </Flex> </Flex> - <Flex width="100%" justifyContent={JUSTIFY_CENTER}> - <PrimaryBtn - data-test="continueButton" - onClick={proceed} - flex="1" - marginX="5rem" - > + <Flex + width="100%" + justifyContent={JUSTIFY_CENTER} + paddingX={chooseTipRackButtonText ? '0' : '5rem'} + > + {chooseTipRackButtonText && ( + <SecondaryBtn + data-test="chooseTipRackButton" + onClick={() => setShowChooseTipRack(true)} + width="48%" + marginRight="1rem" + > + {chooseTipRackButtonText} + </SecondaryBtn> + )} + <PrimaryBtn data-test="continueButton" onClick={proceed} flex="1"> {continueButtonText} </PrimaryBtn> </Flex> diff --git a/app/src/components/CalibrationPanels/__tests__/ChooseTipRack.test.js b/app/src/components/CalibrationPanels/__tests__/ChooseTipRack.test.js new file mode 100644 index 00000000000..a341da3c40b --- /dev/null +++ b/app/src/components/CalibrationPanels/__tests__/ChooseTipRack.test.js @@ -0,0 +1,115 @@ +// @flow +import * as React from 'react' +import { mountWithStore } from '@opentrons/components/__utils__' + +import { mockAttachedPipette } from '../../../pipettes/__fixtures__' +import { mockDeckCalTipRack } from '../../../sessions/__fixtures__' +import { mockTipRackDefinition } from '../../../custom-labware/__fixtures__' +import * as Sessions from '../../../sessions' + +import { + getCalibrationForPipette, + getTipLengthForPipetteAndTiprack, + getTipLengthCalibrations, +} from '../../../calibration' +import { getCustomTipRackDefinitions } from '../../../custom-labware' +import { getAttachedPipettes } from '../../../pipettes' + +import { ChooseTipRack } from '../ChooseTipRack' +import type { State } from '../../../types' +import type { AttachedPipettesByMount } from '../../../pipettes/types' + +jest.mock('../../../pipettes/selectors') +jest.mock('../../../calibration/') +jest.mock('../../../custom-labware/selectors') + +const mockAttachedPipettes: AttachedPipettesByMount = ({ + left: mockAttachedPipette, + right: null, +}: any) + +const mockGetCalibrationForPipette: JestMockFn< + [State, string, string, string], + $Call<typeof getCalibrationForPipette, State, string, string, string> +> = getCalibrationForPipette + +const mockGetTipLengthForPipetteAndTiprack: JestMockFn< + [State, string, string, string], + $Call<typeof getTipLengthForPipetteAndTiprack, State, string, string, string> +> = getTipLengthForPipetteAndTiprack + +const mockGetTipLengthCalibrations: JestMockFn< + [State, string], + $Call<typeof getTipLengthCalibrations, State, string> +> = getTipLengthCalibrations + +const mockGetAttachedPipettes: JestMockFn< + [State, string], + $Call<typeof getAttachedPipettes, State, string> +> = getAttachedPipettes + +const mockGetCustomTipRackDefinitions: JestMockFn< + [State], + $Call<typeof getCustomTipRackDefinitions, State> +> = getCustomTipRackDefinitions + +describe('ChooseTipRack', () => { + let render + + const getUseThisTipRackButton = wrapper => + wrapper.find('button[data-test="useThisTipRackButton"]') + + beforeEach(() => { + mockGetCalibrationForPipette.mockReturnValue(null) + mockGetTipLengthForPipetteAndTiprack.mockReturnValue(null) + mockGetTipLengthCalibrations.mockReturnValue([]) + mockGetAttachedPipettes.mockReturnValue(mockAttachedPipettes) + mockGetCustomTipRackDefinitions.mockReturnValue([ + mockTipRackDefinition, + mockDeckCalTipRack.definition, + ]) + + render = (props: $Shape<React.ElementProps<typeof ChooseTipRack>> = {}) => { + const { + tipRack = mockDeckCalTipRack, + mount = 'left', + sessionType = Sessions.SESSION_TYPE_DECK_CALIBRATION, + chosenTipRack = null, + handleChosenTipRack = jest.fn(), + closeModal = jest.fn(), + robotName = 'opentrons', + } = props + return mountWithStore( + <ChooseTipRack + tipRack={tipRack} + mount={mount} + sessionType={sessionType} + chosenTipRack={chosenTipRack} + handleChosenTipRack={handleChosenTipRack} + closeModal={closeModal} + robotName={robotName} + /> + ) + } + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + it('Deck calibration shows correct text', () => { + const { wrapper } = render() + const allText = wrapper.text() + expect(allText).toContain('calibrate your Deck') + expect(getUseThisTipRackButton(wrapper).exists()).toBe(true) + }) + + it('Pipette offset calibration shows correct text', () => { + const { wrapper } = render({ + sessionType: Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, + }) + const allText = wrapper.text() + expect(allText).toContain('calibrate your Pipette Offset') + expect(getUseThisTipRackButton(wrapper).exists()).toBe(true) + }) +}) diff --git a/app/src/components/CalibrationPanels/__tests__/Introduction.test.js b/app/src/components/CalibrationPanels/__tests__/Introduction.test.js index 1cebf69681a..420b51da717 100644 --- a/app/src/components/CalibrationPanels/__tests__/Introduction.test.js +++ b/app/src/components/CalibrationPanels/__tests__/Introduction.test.js @@ -15,6 +15,8 @@ describe('Introduction', () => { const getContinueButton = wrapper => wrapper.find('button[data-test="continueButton"]') + const getUseDiffTipRackButton = wrapper => + wrapper.find('button[data-test="chooseTipRackButton"]') beforeEach(() => { render = (props: $Shape<React.ElementProps<typeof Introduction>> = {}) => { @@ -27,7 +29,7 @@ describe('Introduction', () => { currentStep = Sessions.DECK_STEP_SESSION_STARTED, sessionType = Sessions.SESSION_TYPE_DECK_CALIBRATION, shouldPerformTipLength = false, - intent = Constants.INTENT_PIPETTE_OFFSET, + intent = Constants.INTENT_CALIBRATE_PIPETTE_OFFSET, } = props return mount( <Introduction @@ -52,11 +54,21 @@ describe('Introduction', () => { const PIP_OFFSET_SPECS = [ { when: 'doing offset only with pipette offset intent', - intent: Constants.INTENT_PIPETTE_OFFSET, + intent: Constants.INTENT_CALIBRATE_PIPETTE_OFFSET, shouldPerformTipLength: false, header: 'pipette offset calibration', body: /calibrating pipette offset/i, note: /using the Opentrons tips/i, + showTipRackButton: false, + }, + { + when: 'doing offset only with recalibrate intent', + intent: Constants.INTENT_RECALIBRATE_PIPETTE_OFFSET, + shouldPerformTipLength: false, + header: 'pipette offset calibration', + body: /calibrating pipette offset/i, + note: /using the Opentrons tips/i, + showTipRackButton: false, }, { when: 'doing offset only with tip length in proto intent', @@ -65,6 +77,7 @@ describe('Introduction', () => { header: 'pipette offset calibration', body: /calibrating pipette offset/i, note: /using the Opentrons tips/i, + showTipRackButton: false, }, { when: 'doing offset only with tip length outside proto intent', @@ -73,22 +86,34 @@ describe('Introduction', () => { header: 'pipette offset calibration', body: /calibrating pipette offset/i, note: /using the Opentrons tips/i, + showTipRackButton: false, }, { when: 'doing fused with pipette offset intent', - intent: Constants.INTENT_PIPETTE_OFFSET, + intent: Constants.INTENT_CALIBRATE_PIPETTE_OFFSET, shouldPerformTipLength: true, header: 'tip length and pipette offset calibration', body: /calibrating pipette offset.*tip length calibration/i, note: /using the Opentrons tips/i, + showTipRackButton: true, }, { - when: 'doing fused with tip length in proto intent', + when: 'doing fused with recalibrate pipette offset intent', + intent: Constants.INTENT_RECALIBRATE_PIPETTE_OFFSET, + shouldPerformTipLength: true, + header: 'tip length and pipette offset calibration', + body: /calibrating pipette offset.*tip length calibration/i, + note: /using the Opentrons tips/i, + showTipRackButton: true, + }, + { + when: 'doing fused with tip length outside proto intent', intent: Constants.INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL, shouldPerformTipLength: true, header: 'tip length and pipette offset calibration', body: /calibrating pipette offset.*tip length calibration/i, note: /using the Opentrons tips/i, + showTipRackButton: true, }, { when: 'doing fused with tip length in proto intent', @@ -97,6 +122,7 @@ describe('Introduction', () => { header: 'tip length and pipette offset calibration', body: /calibrating pipette offset.*tip length calibration/i, note: /using the exact tips/i, + showTipRackButton: false, }, ] PIP_OFFSET_SPECS.forEach(spec => { @@ -110,6 +136,9 @@ describe('Introduction', () => { expect(allText).toContain(spec.header) expect(allText).toMatch(spec.body) expect(allText).toMatch(spec.note) + expect(getUseDiffTipRackButton(wrapper).exists()).toBe( + spec.showTipRackButton + ) getContinueButton(wrapper).invoke('onClick')() wrapper.update() @@ -128,6 +157,7 @@ describe('Introduction', () => { expect(allText).toContain('Deck calibration ensures positional accuracy') expect(allText).toContain('start deck calibration') + expect(getUseDiffTipRackButton(wrapper).exists()).toBe(true) getContinueButton(wrapper).invoke('onClick')() wrapper.update() expect(mockSendCommands).toHaveBeenCalledWith({ @@ -160,6 +190,7 @@ describe('Introduction', () => { ) expect(allText).toContain('start tip length calibration') expect(allText).toMatch(spec.note) + expect(getUseDiffTipRackButton(wrapper).exists()).toBe(false) getContinueButton(wrapper).invoke('onClick')() wrapper.update() @@ -179,6 +210,7 @@ describe('Introduction', () => { expect(allText).toMatch(/diagnoses calibration problems with tip length/i) expect(allText).toMatch(/you will manually guide each attached pipette/i) expect(allText).toMatch(/you will be prompted to recalibrate/i) + expect(getUseDiffTipRackButton(wrapper).exists()).toBe(false) }) it('renders need help link', () => { diff --git a/app/src/components/CalibrationPanels/constants.js b/app/src/components/CalibrationPanels/constants.js index 02febc20cb5..f3a5b7fa137 100644 --- a/app/src/components/CalibrationPanels/constants.js +++ b/app/src/components/CalibrationPanels/constants.js @@ -4,14 +4,18 @@ export const INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL: 'tip-length-no-protocol' = 'tip-length-no-protocol' export const INTENT_TIP_LENGTH_IN_PROTOCOL: 'tip-length-in-protocol' = 'tip-length-in-protocol' -export const INTENT_PIPETTE_OFFSET: 'pipette-offset' = 'pipette-offset' +export const INTENT_CALIBRATE_PIPETTE_OFFSET: 'pipette-offset' = + 'pipette-offset' +export const INTENT_RECALIBRATE_PIPETTE_OFFSET: 'recalibrate-pipette-offset' = + 'recalibrate-pipette-offset' export const INTENT_DECK_CALIBRATION: 'deck-calibration' = 'deck-calibration' export const INTENT_HEALTH_CHECK: 'health-check' = 'health-check' export const INTENTS = [ INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL, INTENT_TIP_LENGTH_IN_PROTOCOL, - INTENT_PIPETTE_OFFSET, + INTENT_CALIBRATE_PIPETTE_OFFSET, + INTENT_RECALIBRATE_PIPETTE_OFFSET, INTENT_DECK_CALIBRATION, INTENT_HEALTH_CHECK, ] diff --git a/app/src/components/CalibrationPanels/styles.css b/app/src/components/CalibrationPanels/styles.css index 8d31a63a44e..15f001ea423 100644 --- a/app/src/components/CalibrationPanels/styles.css +++ b/app/src/components/CalibrationPanels/styles.css @@ -14,3 +14,10 @@ padding: 1rem; max-width: 64rem; } + +.select_tiprack_menu { + & :global(.ot_select__menu) { + max-height: 200px; + overflow: scroll; + } +} diff --git a/app/src/components/CalibrationPanels/types.js b/app/src/components/CalibrationPanels/types.js index 97ad9e56b84..5f8aea0cb51 100644 --- a/app/src/components/CalibrationPanels/types.js +++ b/app/src/components/CalibrationPanels/types.js @@ -11,7 +11,8 @@ import type { import typeof { INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL, INTENT_TIP_LENGTH_IN_PROTOCOL, - INTENT_PIPETTE_OFFSET, + INTENT_CALIBRATE_PIPETTE_OFFSET, + INTENT_RECALIBRATE_PIPETTE_OFFSET, INTENT_DECK_CALIBRATION, INTENT_HEALTH_CHECK, } from './constants' @@ -35,13 +36,14 @@ import typeof { export type Intent = | INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL | INTENT_TIP_LENGTH_IN_PROTOCOL - | INTENT_PIPETTE_OFFSET + | INTENT_CALIBRATE_PIPETTE_OFFSET + | INTENT_RECALIBRATE_PIPETTE_OFFSET | INTENT_DECK_CALIBRATION | INTENT_HEALTH_CHECK export type PipetteOffsetIntent = | INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL | INTENT_TIP_LENGTH_IN_PROTOCOL - | INTENT_PIPETTE_OFFSET + | INTENT_CALIBRATE_PIPETTE_OFFSET export type TipLengthIntent = | INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL | INTENT_TIP_LENGTH_IN_PROTOCOL @@ -65,4 +67,5 @@ export type CalibrationPanelProps = {| comparisonsByPipette?: CalibrationCheckComparisonByPipette | null, activePipette?: CalibrationCheckInstrument, intent?: Intent, + robotName?: string | null, |} diff --git a/app/src/components/ChangePipette/index.js b/app/src/components/ChangePipette/index.js index 1bd7a76001a..42f9dcb4467 100644 --- a/app/src/components/ChangePipette/index.js +++ b/app/src/components/ChangePipette/index.js @@ -26,7 +26,7 @@ import { import { useCalibratePipetteOffset } from '../CalibratePipetteOffset/useCalibratePipetteOffset' import { AskForCalibrationBlockModal } from '../CalibrateTipLength/AskForCalibrationBlockModal' -import { INTENT_PIPETTE_OFFSET } from '../CalibrationPanels' +import { INTENT_CALIBRATE_PIPETTE_OFFSET } from '../CalibrationPanels' import { ClearDeckAlertModal } from '../ClearDeckAlertModal' import { ExitAlertModal } from './ExitAlertModal' import { Instructions } from './Instructions' @@ -130,7 +130,7 @@ export function ChangePipette(props: Props): React.Node { configHasCalibrationBlock ?? hasBlockModalResponse ), }, - withIntent: INTENT_PIPETTE_OFFSET, + withIntent: INTENT_CALIBRATE_PIPETTE_OFFSET, }) setShowCalBlockModal(false) } diff --git a/app/src/components/InstrumentSettings/PipetteCalibrationInfo.js b/app/src/components/InstrumentSettings/PipetteCalibrationInfo.js index 2f4b2a669ef..82a4142ac2b 100644 --- a/app/src/components/InstrumentSettings/PipetteCalibrationInfo.js +++ b/app/src/components/InstrumentSettings/PipetteCalibrationInfo.js @@ -37,7 +37,7 @@ import { useCalibratePipetteOffset } from '../CalibratePipetteOffset/useCalibrat import { InlineCalibrationWarning } from '../InlineCalibrationWarning' import { AskForCalibrationBlockModal } from '../CalibrateTipLength/AskForCalibrationBlockModal' import { - INTENT_PIPETTE_OFFSET, + INTENT_RECALIBRATE_PIPETTE_OFFSET, INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL, } from '../CalibrationPanels' import { formatLastModified } from '../CalibrationPanels/utils' @@ -143,7 +143,7 @@ export function PipetteCalibrationInfo(props: Props): React.Node { shouldRecalibrateTipLength: !keepTipLength, }, withIntent: keepTipLength - ? INTENT_PIPETTE_OFFSET + ? INTENT_RECALIBRATE_PIPETTE_OFFSET : INTENT_TIP_LENGTH_OUTSIDE_PROTOCOL, }) setCalBlockModalState(CAL_BLOCK_MODAL_CLOSED) @@ -230,7 +230,7 @@ export function PipetteCalibrationInfo(props: Props): React.Node { pipetteOffsetCalibration ? () => startPipetteOffsetCalibration({ - withIntent: INTENT_PIPETTE_OFFSET, + withIntent: INTENT_RECALIBRATE_PIPETTE_OFFSET, }) : () => startPipetteOffsetPossibleTLC({ keepTipLength: true }) } diff --git a/app/src/components/InstrumentSettings/__tests__/PipetteInfo.test.js b/app/src/components/InstrumentSettings/__tests__/PipetteInfo.test.js index ec00aecea46..75f6c460ec7 100644 --- a/app/src/components/InstrumentSettings/__tests__/PipetteInfo.test.js +++ b/app/src/components/InstrumentSettings/__tests__/PipetteInfo.test.js @@ -108,7 +108,9 @@ describe('PipetteInfo', () => { const { wrapper } = render() wrapper.find('button[title="pipetteOffsetCalButton"]').invoke('onClick')() wrapper.update() - expect(startWizard).toHaveBeenCalledWith({ withIntent: 'pipette-offset' }) + expect(startWizard).toHaveBeenCalledWith({ + withIntent: 'recalibrate-pipette-offset', + }) }) it('launch POC w/ cal block modal denied if POC button clicked and no existing data and no cal block pref saved', () => { @@ -125,7 +127,7 @@ describe('PipetteInfo', () => { hasCalibrationBlock: true, shouldRecalibrateTipLength: false, }, - withIntent: 'pipette-offset', + withIntent: 'recalibrate-pipette-offset', }) }) @@ -141,7 +143,7 @@ describe('PipetteInfo', () => { hasCalibrationBlock: false, shouldRecalibrateTipLength: false, }, - withIntent: 'pipette-offset', + withIntent: 'recalibrate-pipette-offset', }) }) diff --git a/app/src/custom-labware/__fixtures__/index.js b/app/src/custom-labware/__fixtures__/index.js index 6a415f901ae..06f1fc37b16 100644 --- a/app/src/custom-labware/__fixtures__/index.js +++ b/app/src/custom-labware/__fixtures__/index.js @@ -74,3 +74,26 @@ export const mockDuplicateLabware: Types.DuplicateLabwareFile = { parameters: { ...mockDefinition.parameters, loadName: 'd' }, }, } + +export const mockTipRackDefinition: LabwareDefinition2 = { + version: 1, + schemaVersion: 2, + namespace: 'custom', + metadata: { + displayName: 'Mock TipRack Definition', + displayCategory: 'tipRack', + displayVolumeUnits: 'mL', + }, + dimensions: { xDimension: 0, yDimension: 0, zDimension: 0 }, + cornerOffsetFromSlot: { x: 0, y: 0, z: 0 }, + parameters: { + loadName: 'mock_tiprack_definition', + format: 'mock', + isTiprack: true, + isMagneticModuleCompatible: false, + }, + brand: { brand: 'Opentrons' }, + ordering: [], + wells: {}, + groups: [], +} diff --git a/app/src/custom-labware/selectors.js b/app/src/custom-labware/selectors.js index 9bd5b9d4e6f..0684f64a0db 100644 --- a/app/src/custom-labware/selectors.js +++ b/app/src/custom-labware/selectors.js @@ -4,6 +4,7 @@ import { createSelector } from 'reselect' import sortBy from 'lodash/sortBy' import { getConfig } from '../config' +import { getIsTiprack } from '@opentrons/shared-data' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { State } from '../types' @@ -61,3 +62,10 @@ export const getCustomLabwareDefinitions: State => Array<LabwareDefinition2> = c getValidCustomLabware, labware => labware.map(lw => lw.definition) ) + +export const getCustomTipRackDefinitions: ( + state: State +) => Array<LabwareDefinition2> = createSelector( + getCustomLabwareDefinitions, + labware => labware.filter(lw => getIsTiprack(lw)) +)