1- import { useState } from 'react'
1+ import { useRef , useState } from 'react'
22import { createPortal } from 'react-dom'
33import { useTranslation } from 'react-i18next'
44import isEqual from 'lodash/isEqual'
55
66import {
7+ ALIGN_CENTER ,
78 COLORS ,
89 DIRECTION_COLUMN ,
910 Flex ,
11+ InputField ,
1012 POSITION_FIXED ,
1113 RadioButton ,
1214 SPACING ,
1315 StyledText ,
1416} from '@opentrons/components'
1517import {
18+ ETHANOL_LIQUID_CLASS_NAME ,
1619 FLEX_SINGLE_SLOT_BY_CUTOUT_ID ,
20+ getAllLiquidClassDefs ,
21+ getFlexNameConversion ,
22+ getTipTypeFromTipRackDefinition ,
23+ GLYCEROL_LIQUID_CLASS_NAME ,
24+ linearInterpolate ,
25+ LOW_VOLUME_PIPETTES ,
26+ NONE_LIQUID_CLASS_NAME ,
1727 TRASH_BIN_ADAPTER_FIXTURE ,
1828 WASTE_CHUTE_FIXTURES ,
29+ WATER_LIQUID_CLASS_NAME ,
1930} from '@opentrons/shared-data'
31+ import { getTransferPlanAndReferenceVolumes } from '@opentrons/step-generation'
2032
2133import { getTopPortalEl } from '/app/App/portal'
34+ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard'
2235import { i18n } from '/app/i18n'
2336import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation'
2437import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics'
2538import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics'
2639import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration'
2740
2841import { ACTIONS } from '../constants'
42+ import { getMaxUiFlowRate , getPipetteName } from '../utils'
43+ import { getExtractTiprackTypeFromURI } from '../utils/getExtractTiprackTypeFromURI'
2944
3045import type { Dispatch } from 'react'
31- import type { DeckConfiguration } from '@opentrons/shared-data'
46+ import type { DeckConfiguration , SupportedTip } from '@opentrons/shared-data'
3247import type {
3348 BlowOutLocation ,
3449 FlowRateKind ,
@@ -101,27 +116,31 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
101116 const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial ( )
102117 const deckConfig = useNotifyDeckConfigurationQuery ( ) . data ?? [ ]
103118
104- const [ isBlowOutEnabled , setisBlowOutEnabled ] = useState < boolean > (
105- state . blowOut != null
119+ const keyboardRef = useRef ( null )
120+ const [ isBlowOutEnabled , setIsBlowOutEnabled ] = useState < boolean > (
121+ state . blowOutDispense != null
106122 )
107123 const [ currentStep , setCurrentStep ] = useState < number > ( 1 )
108124 const [ blowOutLocation , setBlowOutLocation ] = useState <
109125 BlowOutLocation | undefined
110- > ( state . blowOut )
126+ > ( state . blowOutDispense ?. location as BlowOutLocation | undefined )
127+ const [ speed , setSpeed ] = useState < number | null > (
128+ ( state . blowOutDispense ?. flowRate as number ) ?? null
129+ )
111130
112131 const enableBlowOutDisplayItems = [
113132 {
114133 option : true ,
115134 description : t ( 'option_enabled' ) ,
116135 onClick : ( ) => {
117- setisBlowOutEnabled ( true )
136+ setIsBlowOutEnabled ( true )
118137 } ,
119138 } ,
120139 {
121140 option : false ,
122141 description : t ( 'option_disabled' ) ,
123142 onClick : ( ) => {
124- setisBlowOutEnabled ( false )
143+ setIsBlowOutEnabled ( false )
125144 } ,
126145 } ,
127146 ]
@@ -140,7 +159,10 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
140159 if ( ! isBlowOutEnabled ) {
141160 dispatch ( {
142161 type : ACTIONS . SET_BLOW_OUT ,
143- location : undefined ,
162+ blowOutSettings : {
163+ location : undefined ,
164+ flowRate : 0 ,
165+ } ,
144166 } )
145167 trackEventWithRobotSerial ( {
146168 name : ANALYTICS_QUICK_TRANSFER_SETTING_SAVED ,
@@ -152,10 +174,17 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
152174 } else {
153175 setCurrentStep ( currentStep + 1 )
154176 }
177+ } else if ( currentStep === 2 ) {
178+ if ( blowOutLocation != null ) {
179+ setCurrentStep ( currentStep + 1 )
180+ }
155181 } else {
156182 dispatch ( {
157183 type : ACTIONS . SET_BLOW_OUT ,
158- location : blowOutLocation ,
184+ blowOutSettings : {
185+ location : blowOutLocation ,
186+ flowRate : speed ?? 1 ,
187+ } ,
159188 } )
160189 trackEventWithRobotSerial ( {
161190 name : ANALYTICS_QUICK_TRANSFER_SETTING_SAVED ,
@@ -168,15 +197,121 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
168197 }
169198
170199 const saveOrContinueButtonText =
171- isBlowOutEnabled && currentStep < 2
200+ isBlowOutEnabled && currentStep < 3
172201 ? t ( 'shared:continue' )
173202 : t ( 'shared:save' )
174203
175204 let buttonIsDisabled = false
176- if ( currentStep === 2 ) {
205+ if ( currentStep === 3 ) {
177206 buttonIsDisabled = blowOutLocation == null
178207 }
179208
209+ const pipetteName = getPipetteName ( state . pipette )
210+ const liquidSpecs = state . pipette . liquids
211+ const tipType = getTipTypeFromTipRackDefinition ( state . tipRack )
212+ const flowRatesForSupportedTip : SupportedTip | undefined =
213+ state . volume < 5 &&
214+ `lowVolumeDefault` in liquidSpecs &&
215+ LOW_VOLUME_PIPETTES . includes ( pipetteName as string )
216+ ? liquidSpecs . lowVolumeDefault . supportedTips [ tipType ]
217+ : liquidSpecs . default . supportedTips [ tipType ]
218+
219+ const allLiquidClassDefs = getAllLiquidClassDefs ( )
220+ const liquidClassMap = new Map < string , string > ( [
221+ [ 'none' , NONE_LIQUID_CLASS_NAME ] ,
222+ [ 'water' , WATER_LIQUID_CLASS_NAME ] ,
223+ [ 'glycerol_50' , GLYCEROL_LIQUID_CLASS_NAME ] ,
224+ [ 'ethanol_80' , ETHANOL_LIQUID_CLASS_NAME ] ,
225+ ] )
226+
227+ const selectedLiquidClass = liquidClassMap . get (
228+ state . liquidClass ?. liquidClassName ?? 'none'
229+ )
230+ const liquidClassDef =
231+ allLiquidClassDefs [ selectedLiquidClass ?? NONE_LIQUID_CLASS_NAME ]
232+ const convertedPipetteName =
233+ state . pipette != null ? getFlexNameConversion ( state . pipette ) : null
234+
235+ const minFlowRate = 1
236+ const { loadName : currentTiprackLoadName } = state . tipRack . parameters
237+
238+ const tipTypeSettings = liquidClassDef ?. byPipette
239+ ?. find ( ( { pipetteModel } ) => convertedPipetteName === pipetteModel )
240+ ?. byTipType . find ( tipObject => {
241+ const tiprackLoadName = tipObject . tiprack . split ( '/' ) [ 1 ]
242+ return tiprackLoadName === currentTiprackLoadName
243+ } )
244+
245+ const correctionByVolume = tipTypeSettings ?. singleDispense ?. correctionByVolume
246+ const retract = tipTypeSettings ?. singleDispense ?. retract
247+
248+ const referenceVolumesForByVolumeInterpolation = getTransferPlanAndReferenceVolumes (
249+ {
250+ pipetteSpecs : state . pipette ,
251+ volume : state . volume ,
252+ tiprackDefinition : state . tipRack ,
253+ path : state . path ,
254+ numDispenseWells : state . destinationWells . length ,
255+ aspirateAirGapByVolume :
256+ ( retract ?. airGapByVolume as Array < [ number , number ] > ) ?? null ,
257+ conditioningByVolume :
258+ ( correctionByVolume as Array < [ number , number ] > ) ?? null ,
259+ disposalByVolume : null , // note always null because blowout is available only for single dispense
260+ }
261+ )
262+
263+ const [ referenceVolumeFlowRate , referenceVolumeCorrection ] = [
264+ referenceVolumesForByVolumeInterpolation . referenceVolumes ?. flowRate
265+ . dispense ,
266+ referenceVolumesForByVolumeInterpolation . referenceVolumes ?. correction
267+ . dispense ,
268+ ]
269+
270+ const liquidClassValuesForPipette = liquidClassDef ?. byPipette ?. find (
271+ ( { pipetteModel } ) => convertedPipetteName === pipetteModel
272+ )
273+ const liquidClassValuesForTip = getExtractTiprackTypeFromURI (
274+ liquidClassValuesForPipette ,
275+ currentTiprackLoadName
276+ )
277+
278+ const correctionVolume =
279+ referenceVolumeCorrection != null &&
280+ ( liquidClassValuesForTip ?. singleDispense ?. correctionByVolume ?. length ?? 0 ) >
281+ 0
282+ ? linearInterpolate (
283+ referenceVolumeCorrection ,
284+ liquidClassValuesForTip ?. singleDispense ?. correctionByVolume as Array <
285+ [ number , number ]
286+ >
287+ )
288+ : 0
289+
290+ const maxFlowRate = getMaxUiFlowRate ( {
291+ targetVolume : referenceVolumeFlowRate ,
292+ channels : state . pipette . channels ,
293+ tipLiquidSpecs : flowRatesForSupportedTip ,
294+ flowRateType : 'blowout' ,
295+ correctionVolume : correctionVolume ?? 0 ,
296+ shaftULperMM : state . pipette . shaftULperMM ,
297+ } )
298+
299+ const speedError =
300+ speed != null && ( speed < minFlowRate || speed > maxFlowRate )
301+ ? t ( `value_out_of_range` , {
302+ min : minFlowRate ,
303+ max : maxFlowRate ,
304+ } )
305+ : null
306+
307+ const handleFlowRateChange = ( userInput : string ) : void => {
308+ if ( userInput === '' ) {
309+ setSpeed ( null )
310+ }
311+ const parsedFlowRate = parseInt ( userInput )
312+ setSpeed ( ! isNaN ( parsedFlowRate ) ? parsedFlowRate : null )
313+ }
314+
180315 return createPortal (
181316 < Flex position = { POSITION_FIXED } backgroundColor = { COLORS . white } width = "100%" >
182317 < ChildNavigation
@@ -244,6 +379,47 @@ export function BlowOut(props: BlowOutProps): JSX.Element {
244379 </ Flex >
245380 </ Flex >
246381 ) : null }
382+ { currentStep === 3 ? (
383+ < Flex
384+ alignSelf = { ALIGN_CENTER }
385+ gridGap = { SPACING . spacing48 }
386+ paddingX = { SPACING . spacing40 }
387+ padding = { `${ SPACING . spacing16 } ${ SPACING . spacing40 } ${ SPACING . spacing40 } ` }
388+ marginTop = "7.75rem" // using margin rather than justify due to content moving with error message
389+ alignItems = { ALIGN_CENTER }
390+ height = "22rem"
391+ >
392+ < Flex
393+ width = "30.5rem"
394+ height = "100%"
395+ gridGap = { SPACING . spacing24 }
396+ flexDirection = { DIRECTION_COLUMN }
397+ marginTop = { SPACING . spacing68 }
398+ >
399+ < InputField
400+ type = "text"
401+ value = { String ( speed ?? '' ) }
402+ title = { t ( 'blow_out_speed' ) }
403+ error = { speedError }
404+ readOnly
405+ />
406+ </ Flex >
407+ < Flex
408+ paddingX = { SPACING . spacing24 }
409+ height = "21.25rem"
410+ marginTop = "7.75rem"
411+ borderRadius = "0"
412+ >
413+ < NumericalKeyboard
414+ keyboardRef = { keyboardRef }
415+ initialValue = { String ( speed ?? '' ) }
416+ onChange = { e => {
417+ handleFlowRateChange ( e )
418+ } }
419+ />
420+ </ Flex >
421+ </ Flex >
422+ ) : null }
247423 </ Flex > ,
248424 getTopPortalEl ( )
249425 )
0 commit comments