+
Todo: wire this up
+import { useTranslation } from 'react-i18next'
+import { useState } from 'react'
+import {
+ DIRECTION_COLUMN,
+ Divider,
+ Flex,
+ SPACING,
+ StyledText,
+ Tabs,
+} from '@opentrons/components'
+import {
+ CheckboxExpandStepFormField,
+ InputStepFormField,
+} from '../../../../molecules'
+import {
+ getBlowoutLocationOptionsForForm,
+ getLabwareFieldForPositioningField,
+} from '../StepForm/utils'
+import {
+ BlowoutLocationField,
+ BlowoutOffsetField,
+ FlowRateField,
+ PositionField,
+ WellsOrderField,
+} from '../StepForm/PipetteFields'
+import type { WellOrderOption } from '../../../../form-types'
+import type { FieldPropsByName } from '../StepForm/types'
+
+interface BatchEditMoveLiquidProps {
+ propsForFields: FieldPropsByName
+}
+
+export function BatchEditMoveLiquidTools(
+ props: BatchEditMoveLiquidProps
+): JSX.Element {
+ const { t, i18n } = useTranslation(['button', 'tooltip', 'protocol_steps'])
+ const { propsForFields } = props
+ const [tab, setTab] = useState<'aspirate' | 'dispense'>('aspirate')
+ const aspirateTab = {
+ text: t('protocol_steps:aspirate'),
+ isActive: tab === 'aspirate',
+ onClick: () => {
+ setTab('aspirate')
+ },
+ }
+ const dispenseTab = {
+ text: t('protocol_steps:dispense'),
+
+ isActive: tab === 'dispense',
+ onClick: () => {
+ setTab('dispense')
+ },
+ }
+ const addFieldNamePrefix = (name: string): string => `${tab}_${name}`
+ const getPipetteIdForForm = (): string | null => {
+ const pipetteId = propsForFields.pipette?.value
+ return pipetteId ? String(pipetteId) : null
+ }
+ const getLabwareIdForPositioningField = (name: string): string | null => {
+ const labwareField = getLabwareFieldForPositioningField(name)
+ const labwareId = propsForFields[labwareField]?.value
+ return labwareId ? String(labwareId) : null
+ }
+ const getWellOrderFieldValue = (
+ name: string
+ ): WellOrderOption | null | undefined => {
+ const val = propsForFields[name]?.value
+ if (val === 'l2r' || val === 'r2l' || val === 't2b' || val === 'b2t') {
+ return val
+ } else {
+ return null
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('protocol_steps:advanced_settings')}
+
+ {tab === 'aspirate' ? (
+
+ ) : null}
+
+ {propsForFields[`${tab}_mix_checkbox`].value === true ? (
+
+
+
+
+ ) : null}
+
+
+ {propsForFields[`${tab}_delay_checkbox`].value === true ? (
+
+
+
+
+ ) : null}
+
+ {tab === 'dispense' ? (
+
+ {propsForFields.blowout_checkbox.value === true ? (
+
+
+
+
+
+ ) : null}
+
+ ) : null}
+
+
+ )
}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/index.tsx
index f753a018e2d..a2fe674db2e 100644
--- a/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/index.tsx
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/index.tsx
@@ -16,16 +16,18 @@ import {
import { useKitchen } from '../../../../organisms/Kitchen/hooks'
import { deselectAllSteps } from '../../../../ui/steps/actions/actions'
import {
- // changeBatchEditField,
+ changeBatchEditField,
resetBatchEditFieldChanges,
saveStepFormsMulti,
} from '../../../../step-forms/actions'
+import { maskField } from '../../../../steplist/fieldLevel'
+import { getBatchEditFormHasUnsavedChanges } from '../../../../step-forms/selectors'
+import { makeBatchEditFieldProps } from './utils'
import { BatchEditMoveLiquidTools } from './BatchEditMoveLiquidTools'
import { BatchEditMixTools } from './BatchEditMixTools'
-// import { maskField } from '../../../../steplist/fieldLevel'
-// import type { StepFieldName } from '../../../../steplist/fieldLevel'
import type { ThunkDispatch } from 'redux-thunk'
+import type { StepFieldName } from '../../../../steplist/fieldLevel'
import type { BaseState } from '../../../../types'
export const BatchEditToolbox = (): JSX.Element | null => {
@@ -36,15 +38,16 @@ export const BatchEditToolbox = (): JSX.Element | null => {
const stepTypes = useSelector(getBatchEditSelectedStepTypes)
const disabledFields = useSelector(getMultiSelectDisabledFields)
const selectedStepIds = useSelector(getMultiSelectItemIds)
+ const batchEditFormHasChanges = useSelector(getBatchEditFormHasUnsavedChanges)
- // const handleChangeFormInput = (name: StepFieldName, value: unknown): void => {
- // const maskedValue = maskField(name, value)
- // dispatch(changeBatchEditField({ [name]: maskedValue }))
- // }
+ const handleChangeFormInput = (name: StepFieldName, value: unknown): void => {
+ const maskedValue = maskField(name, value)
+ dispatch(changeBatchEditField({ [name]: maskedValue }))
+ }
const handleSave = (): void => {
dispatch(saveStepFormsMulti(selectedStepIds))
- makeSnackbar(t('batch_edits_saved') as string)
+ makeSnackbar(t('protocol_steps:batch_edits_saved') as string)
dispatch(deselectAllSteps('EXIT_BATCH_EDIT_MODE_BUTTON_PRESS'))
}
@@ -56,12 +59,12 @@ export const BatchEditToolbox = (): JSX.Element | null => {
const stepType = stepTypes.length === 1 ? stepTypes[0] : null
if (stepType !== null && fieldValues !== null && disabledFields !== null) {
- // const propsForFields = makeBatchEditFieldProps(
- // fieldValues,
- // disabledFields,
- // handleChangeFormInput,
- // t
- // )
+ const propsForFields = makeBatchEditFieldProps(
+ fieldValues,
+ disabledFields,
+ handleChangeFormInput,
+ t
+ )
if (stepType === 'moveLiquid' || stepType === 'mix') {
return (
{
onCloseClick={handleCancel}
closeButton={}
confirmButton={
-
+
{t('shared:save')}
}
>
{stepType === 'moveLiquid' ? (
-
+
) : (
)}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/BlowoutLocationField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/BlowoutLocationField.tsx
new file mode 100644
index 00000000000..f603e5dc119
--- /dev/null
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/BlowoutLocationField.tsx
@@ -0,0 +1,29 @@
+import { useSelector } from 'react-redux'
+import { useTranslation } from 'react-i18next'
+import { selectors as uiLabwareSelectors } from '../../../../../ui/labware'
+import { DropdownStepFormField } from '../../../../../molecules'
+import type { Options } from '@opentrons/components'
+import type { FieldProps } from '../types'
+
+type BlowoutLocationDropdownProps = FieldProps & {
+ options: Options
+}
+
+export function BlowoutLocationField(
+ props: BlowoutLocationDropdownProps
+): JSX.Element {
+ const { options: propOptions, ...restProps } = props
+ const { t } = useTranslation('protocol_steps')
+ const disposalOptions = useSelector(uiLabwareSelectors.getDisposalOptions)
+ const options = [...disposalOptions, ...propOptions]
+
+ return (
+
+ )
+}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/BlowoutOffsetField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/BlowoutOffsetField.tsx
new file mode 100644
index 00000000000..8678a558f62
--- /dev/null
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/BlowoutOffsetField.tsx
@@ -0,0 +1,91 @@
+import { useState } from 'react'
+import { useSelector } from 'react-redux'
+import { useTranslation } from 'react-i18next'
+import {
+ DEST_WELL_BLOWOUT_DESTINATION,
+ SOURCE_WELL_BLOWOUT_DESTINATION,
+} from '@opentrons/step-generation'
+import { getWellDepth } from '@opentrons/shared-data'
+import {
+ Flex,
+ InputField,
+ Tooltip,
+ useHoverTooltip,
+} from '@opentrons/components'
+import { ZTipPositionModal } from '../../../../../components/StepEditForm/fields/TipPositionField/ZTipPositionModal'
+import { getLabwareEntities } from '../../../../../step-forms/selectors'
+import type { FieldProps } from '../types'
+
+interface BlowoutOffsetFieldProps extends FieldProps {
+ destLabwareId: unknown
+ sourceLabwareId?: unknown
+ blowoutLabwareId?: unknown
+}
+
+export function BlowoutOffsetField(
+ props: BlowoutOffsetFieldProps
+): JSX.Element {
+ const {
+ disabled,
+ value,
+ destLabwareId,
+ sourceLabwareId,
+ blowoutLabwareId,
+ tooltipContent,
+ name,
+ isIndeterminate,
+ updateValue,
+ } = props
+ const { t } = useTranslation(['application', 'protocol_steps'])
+ const [isModalOpen, setModalOpen] = useState(false)
+ const [targetProps, tooltipProps] = useHoverTooltip()
+ const labwareEntities = useSelector(getLabwareEntities)
+
+ let labwareId = null
+ if (blowoutLabwareId === SOURCE_WELL_BLOWOUT_DESTINATION) {
+ labwareId = sourceLabwareId
+ } else if (blowoutLabwareId === DEST_WELL_BLOWOUT_DESTINATION) {
+ labwareId = destLabwareId
+ }
+
+ const labwareWellDepth =
+ labwareId != null && labwareEntities[String(labwareId)]?.def != null
+ ? getWellDepth(labwareEntities[String(labwareId)].def, 'A1')
+ : 0
+
+ return (
+ <>
+ {tooltipContent}
+ {isModalOpen ? (
+ {
+ setModalOpen(false)
+ }}
+ name={name}
+ zValue={Number(value)}
+ updateValue={updateValue}
+ wellDepthMm={labwareWellDepth}
+ />
+ ) : null}
+
+
+ {
+ setModalOpen(true)
+ }
+ }
+ value={String(value)}
+ isIndeterminate={isIndeterminate}
+ units={t('units.millimeter')}
+ id={`TipPositionField_${name}`}
+ />
+
+ >
+ )
+}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx
new file mode 100644
index 00000000000..325f0639ac2
--- /dev/null
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx
@@ -0,0 +1,115 @@
+import { useTranslation } from 'react-i18next'
+import { useSelector } from 'react-redux'
+import { Flex, DIRECTION_COLUMN, SPACING } from '@opentrons/components'
+import { getMaxDisposalVolumeForMultidispense } from '../../../../../steplist/formLevel/handleFormChange/utils'
+import { selectors as stepFormSelectors } from '../../../../../step-forms'
+import { selectors as uiLabwareSelectors } from '../../../../../ui/labware'
+import {
+ CheckboxExpandStepFormField,
+ DropdownStepFormField,
+ InputStepFormField,
+} from '../../../../../molecules'
+import { getBlowoutLocationOptionsForForm } from '../utils'
+import { FlowRateField } from './FlowRateField'
+import { BlowoutOffsetField } from './BlowoutOffsetField'
+
+import type { PathOption, StepType } from '../../../../../form-types'
+import type { FieldPropsByName } from '../types'
+
+interface DisposalFieldProps {
+ path: PathOption
+ pipette: string | null
+ propsForFields: FieldPropsByName
+ stepType: StepType
+ volume: string | null
+ aspirate_airGap_checkbox?: boolean | null
+ aspirate_airGap_volume?: string | null
+ tipRack?: string | null
+}
+
+export function DisposalField(props: DisposalFieldProps): JSX.Element {
+ const {
+ path,
+ stepType,
+ volume,
+ pipette,
+ propsForFields,
+ aspirate_airGap_checkbox,
+ aspirate_airGap_volume,
+ tipRack,
+ } = props
+ const { t } = useTranslation(['application', 'form'])
+
+ const disposalOptions = useSelector(uiLabwareSelectors.getDisposalOptions)
+ const pipetteEntities = useSelector(stepFormSelectors.getPipetteEntities)
+ const blowoutLocationOptions = getBlowoutLocationOptionsForForm({
+ path,
+ stepType,
+ })
+ const maxDisposalVolume = getMaxDisposalVolumeForMultidispense(
+ {
+ aspirate_airGap_checkbox,
+ aspirate_airGap_volume,
+ path,
+ pipette,
+ volume,
+ tipRack,
+ },
+ pipetteEntities
+ )
+ const disposalDestinationOptions = [
+ ...disposalOptions,
+ ...blowoutLocationOptions,
+ ]
+
+ const volumeBoundsCaption =
+ maxDisposalVolume != null
+ ? t('protocol_steps:max_disposal_volume', {
+ vol: maxDisposalVolume,
+ unit: t('units.microliter'),
+ })
+ : ''
+
+ const { value, updateValue } = propsForFields.disposalVolume_checkbox
+ return (
+
+ {value ? (
+
+
+
+
+
+
+ ) : null}
+
+ )
+}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx
new file mode 100644
index 00000000000..a89c4f0be62
--- /dev/null
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx
@@ -0,0 +1,63 @@
+import { useSelector } from 'react-redux'
+import { useTranslation } from 'react-i18next'
+import { selectors as stepFormSelectors } from '../../../../../step-forms'
+import { getMatchingTipLiquidSpecs } from '../../../../../utils'
+import { FlowRateInput } from './FlowRateInput'
+import type { FieldProps } from '../types'
+import type { FlowRateInputProps } from './FlowRateInput'
+
+interface FlowRateFieldProps extends FieldProps {
+ flowRateType: FlowRateInputProps['flowRateType']
+ volume: unknown
+ tiprack: unknown
+ pipetteId?: string | null
+}
+
+export function FlowRateField(props: FlowRateFieldProps): JSX.Element {
+ const {
+ pipetteId,
+ flowRateType,
+ value,
+ volume,
+ tiprack,
+ name,
+ ...passThruProps
+ } = props
+ const { t } = useTranslation('shared')
+ const pipetteEntities = useSelector(stepFormSelectors.getPipetteEntities)
+ const pipette = pipetteId != null ? pipetteEntities[pipetteId] : null
+ const pipetteDisplayName = pipette ? pipette.spec.displayName : t('pipette')
+ const innerKey = `${name}:${String(value || 0)}`
+ const matchingTipLiquidSpecs =
+ pipette != null
+ ? getMatchingTipLiquidSpecs(pipette, volume as number, tiprack as string)
+ : null
+
+ let defaultFlowRate
+ if (pipette) {
+ if (flowRateType === 'aspirate') {
+ defaultFlowRate =
+ matchingTipLiquidSpecs?.defaultAspirateFlowRate.default ?? 0
+ } else if (flowRateType === 'dispense') {
+ defaultFlowRate =
+ matchingTipLiquidSpecs?.defaultDispenseFlowRate.default ?? 0
+ } else if (flowRateType === 'blowout') {
+ defaultFlowRate =
+ matchingTipLiquidSpecs?.defaultBlowOutFlowRate.default ?? 0
+ }
+ }
+ return (
+
+ )
+}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateInput.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateInput.tsx
new file mode 100644
index 00000000000..210f831bb86
--- /dev/null
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateInput.tsx
@@ -0,0 +1,255 @@
+import { useState } from 'react'
+import { createPortal } from 'react-dom'
+import round from 'lodash/round'
+import { useTranslation } from 'react-i18next'
+import {
+ RadioGroup,
+ Flex,
+ useHoverTooltip,
+ InputField,
+ Modal,
+ SecondaryButton,
+ PrimaryButton,
+ Tooltip,
+} from '@opentrons/components'
+import { getMainPagePortalEl } from '../../../../../components/portals/MainPageModalPortal'
+import type { ChangeEvent } from 'react'
+import type { FieldProps } from '../types'
+
+const DECIMALS_ALLOWED = 1
+
+export interface FlowRateInputProps extends FieldProps {
+ flowRateType: 'aspirate' | 'dispense' | 'blowout'
+ minFlowRate: number
+ maxFlowRate: number
+ defaultFlowRate?: number | null
+ pipetteDisplayName?: string | null
+}
+
+interface InitialState {
+ isPristine: boolean
+ modalUseDefault: boolean
+ showModal: boolean
+ modalFlowRate?: string | null
+}
+
+export function FlowRateInput(props: FlowRateInputProps): JSX.Element {
+ const {
+ defaultFlowRate,
+ disabled,
+ flowRateType,
+ isIndeterminate,
+ maxFlowRate,
+ minFlowRate,
+ name,
+ pipetteDisplayName,
+ tooltipContent,
+ value,
+ } = props
+ const [targetProps, tooltipProps] = useHoverTooltip()
+ const { t, i18n } = useTranslation([
+ 'form',
+ 'application',
+ 'shared',
+ 'protocol_steps',
+ ])
+
+ const initialState: InitialState = {
+ isPristine: true,
+ modalFlowRate: props.value ? String(props.value) : null,
+ modalUseDefault: !props.value && !isIndeterminate,
+ showModal: false,
+ }
+
+ const [isPristine, setIsPristine] = useState(
+ initialState.isPristine
+ )
+
+ const [modalFlowRate, setModalFlowRate] = useState<
+ InitialState['modalFlowRate']
+ >(initialState.modalFlowRate)
+
+ const [modalUseDefault, setModalUseDefault] = useState<
+ InitialState['modalUseDefault']
+ >(initialState.modalUseDefault)
+
+ const [showModal, setShowModal] = useState(
+ initialState.showModal
+ )
+
+ const resetModalState = (): void => {
+ setShowModal(initialState.showModal)
+ setModalFlowRate(initialState.modalFlowRate)
+ setModalUseDefault(initialState.modalUseDefault)
+ setIsPristine(initialState.isPristine)
+ }
+
+ const cancelModal = resetModalState
+
+ const openModal = (): void => {
+ setShowModal(true)
+ }
+
+ const makeSaveModal = (allowSave: boolean) => (): void => {
+ setIsPristine(false)
+
+ if (allowSave) {
+ const newFlowRate = modalUseDefault ? null : Number(modalFlowRate)
+ props.updateValue(newFlowRate)
+ resetModalState()
+ }
+ }
+
+ const handleChangeRadio = (e: ChangeEvent): void => {
+ setModalUseDefault(e.target.value !== 'custom')
+ }
+
+ const handleChangeNumber = (e: ChangeEvent): void => {
+ const value = e.target.value
+ if (value === '' || value === '.' || !Number.isNaN(Number(value))) {
+ setModalFlowRate(value)
+ setModalUseDefault(false)
+ }
+ }
+ const title = i18n.format(
+ t('protocol_steps:flow_type_title', { type: flowRateType }),
+ 'capitalize'
+ )
+
+ const modalFlowRateNum = Number(modalFlowRate)
+
+ // show 0.1 not 0 as minimum, since bottom of range is non-inclusive
+ const displayMinFlowRate = minFlowRate || Math.pow(10, -DECIMALS_ALLOWED)
+ const rangeDescription = t('step_edit_form.field.flow_rate.range', {
+ min: displayMinFlowRate,
+ max: maxFlowRate,
+ })
+ const outOfBounds =
+ modalFlowRateNum === 0 ||
+ minFlowRate > modalFlowRateNum ||
+ modalFlowRateNum > maxFlowRate
+ const correctDecimals =
+ round(modalFlowRateNum, DECIMALS_ALLOWED) === modalFlowRateNum
+ const allowSave = modalUseDefault || (!outOfBounds && correctDecimals)
+
+ let errorMessage = null
+ // validation only happens when "Custom" is selected, not "Default"
+ // and pristinity only masks the outOfBounds error, not the correctDecimals error
+ if (!modalUseDefault) {
+ if (!Number.isNaN(modalFlowRateNum) && !correctDecimals) {
+ errorMessage = t('step_edit_form.field.flow_rate.error_decimals', {
+ decimals: `${DECIMALS_ALLOWED}`,
+ })
+ } else if (!isPristine && outOfBounds) {
+ errorMessage = t('step_edit_form.field.flow_rate.error_out_of_bounds', {
+ min: displayMinFlowRate,
+ max: maxFlowRate,
+ })
+ }
+ }
+
+ const FlowRateInputField = (
+
+ )
+
+ // TODO: update the modal
+ const FlowRateModal =
+ pipetteDisplayName &&
+ createPortal(
+
+
+ {t('shared:cancel')}
+
+
+ {t('shared:done')}
+
+
+ }
+ >
+ {t('protocol_steps:flow_type_title', { type: flowRateType })}
+
+ {title}
+
+ {`${flowRateType} speed`}
+
+
+ ,
+ getMainPagePortalEl()
+ )
+
+ return (
+ <>
+ {flowRateType === 'blowout' ? (
+
+
+ {tooltipContent}
+
+ ) : (
+
+
+
+ )}
+
+ {showModal && FlowRateModal}
+ >
+ )
+}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx
new file mode 100644
index 00000000000..720a273d1a2
--- /dev/null
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx
@@ -0,0 +1,212 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useSelector } from 'react-redux'
+import {
+ COLORS,
+ DIRECTION_COLUMN,
+ Flex,
+ InputField,
+ ListButton,
+ SPACING,
+ StyledText,
+ Tooltip,
+ useHoverTooltip,
+} from '@opentrons/components'
+import { getWellsDepth, getWellDimension } from '@opentrons/shared-data'
+import { getIsDelayPositionField } from '../../../../../form-types'
+import { selectors as stepFormSelectors } from '../../../../../step-forms'
+import { TipPositionModal } from '../../../../../components/StepEditForm/fields/TipPositionField/TipPositionModal'
+import { getDefaultMmFromBottom } from '../../../../../components/StepEditForm/fields/TipPositionField/utils'
+import { ZTipPositionModal } from '../../../../../components/StepEditForm/fields/TipPositionField/ZTipPositionModal'
+import type {
+ TipXOffsetFields,
+ TipYOffsetFields,
+ TipZOffsetFields,
+} from '../../../../../form-types'
+import type { FieldPropsByName } from '../types'
+import type { PositionSpecs } from '../../../../../components/StepEditForm/fields/TipPositionField/TipPositionModal'
+interface PositionFieldProps {
+ prefix: 'aspirate' | 'dispense'
+ propsForFields: FieldPropsByName
+ zField: TipZOffsetFields
+ xField?: TipXOffsetFields
+ yField?: TipYOffsetFields
+ labwareId?: string | null
+}
+
+export function PositionField(props: PositionFieldProps): JSX.Element {
+ const { labwareId, propsForFields, zField, xField, yField, prefix } = props
+ const {
+ name: zName,
+ value: rawZValue,
+ updateValue: zUpdateValue,
+ tooltipContent,
+ isIndeterminate,
+ disabled,
+ } = propsForFields[zField]
+
+ const { t, i18n } = useTranslation(['application', 'protocol_steps'])
+ const [targetProps, tooltipProps] = useHoverTooltip()
+ const [isModalOpen, setModalOpen] = useState(false)
+ const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities)
+ const labwareDef =
+ labwareId != null && labwareEntities[labwareId] != null
+ ? labwareEntities[labwareId].def
+ : null
+
+ let wellDepthMm = 0
+ let wellXWidthMm = 0
+ let wellYWidthMm = 0
+
+ if (labwareDef != null) {
+ // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths/widths
+ const firstWell = labwareDef.wells.A1
+ if (firstWell) {
+ wellDepthMm = getWellsDepth(labwareDef, ['A1'])
+ wellXWidthMm = getWellDimension(labwareDef, ['A1'], 'x')
+ wellYWidthMm = getWellDimension(labwareDef, ['A1'], 'y')
+ }
+ }
+
+ if (
+ (wellDepthMm === 0 || wellXWidthMm === 0 || wellYWidthMm === 0) &&
+ labwareId != null &&
+ labwareDef != null
+ ) {
+ console.error(
+ `expected to find all well dimensions mm with labwareId ${labwareId} but could not`
+ )
+ }
+
+ const handleOpen = (has3Specs: boolean): void => {
+ if (has3Specs && wellDepthMm && wellXWidthMm && wellYWidthMm) {
+ setModalOpen(true)
+ }
+ if (!has3Specs && wellDepthMm) {
+ setModalOpen(true)
+ }
+ }
+ const handleClose = (): void => {
+ setModalOpen(false)
+ }
+ const isDelayPositionField = getIsDelayPositionField(zName)
+ let zValue: string | number = '0'
+ const mmFromBottom = typeof rawZValue === 'number' ? rawZValue : null
+ if (wellDepthMm !== null) {
+ // show default value for field in parens if no mmFromBottom value is selected
+ zValue =
+ mmFromBottom ?? getDefaultMmFromBottom({ name: zName, wellDepthMm })
+ }
+ let modal = (
+
+ )
+ if (yField != null && xField != null) {
+ const {
+ name: xName,
+ value: rawXValue,
+ updateValue: xUpdateValue,
+ } = propsForFields[xField]
+ const {
+ name: yName,
+ value: rawYValue,
+ updateValue: yUpdateValue,
+ } = propsForFields[yField]
+
+ const specs: PositionSpecs = {
+ z: {
+ name: zName,
+ value: mmFromBottom,
+ updateValue: zUpdateValue,
+ },
+ x: {
+ name: xName,
+ value: rawXValue != null ? Number(rawXValue) : null,
+ updateValue: xUpdateValue,
+ },
+ y: {
+ name: yName,
+ value: rawYValue != null ? Number(rawYValue) : null,
+ updateValue: yUpdateValue,
+ },
+ }
+
+ modal = (
+
+ )
+ }
+
+ return (
+ <>
+ {tooltipContent}
+ {isModalOpen ? modal : null}
+ {yField != null && xField != null ? (
+
+
+ {i18n.format(
+ t('protocol_steps:tip_position', { prefix }),
+ 'capitalize'
+ )}
+
+ {
+ handleOpen(true)
+ }}
+ >
+
+ {t('protocol_steps:well_position')}
+ {`${
+ propsForFields[xField].value != null
+ ? Number(propsForFields[xField].value)
+ : 0
+ }${t('units.millimeter')},
+ ${
+ propsForFields[yField].value != null
+ ? Number(propsForFields[yField].value)
+ : 0
+ }${t('units.millimeter')},
+ ${mmFromBottom ?? 0}${t('units.millimeter')}`}
+
+
+
+ ) : (
+ {
+ handleOpen(false)
+ }}
+ value={String(zValue)}
+ isIndeterminate={isIndeterminate}
+ units={t('units.millimeter')}
+ id={`TipPositionField_${zName}`}
+ />
+ )}
+ >
+ )
+}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellsOrderField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellsOrderField.tsx
new file mode 100644
index 00000000000..a4152db7d4b
--- /dev/null
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellsOrderField.tsx
@@ -0,0 +1,96 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ useHoverTooltip,
+ Tooltip,
+ ListButton,
+ StyledText,
+ Flex,
+ SPACING,
+ DIRECTION_COLUMN,
+ COLORS,
+} from '@opentrons/components'
+import { WellOrderModal } from '../../../../../components/StepEditForm/fields/WellOrderField/WellOrderModal'
+import type { WellOrderOption } from '../../../../../form-types'
+import type { FieldProps } from '../types'
+
+export interface WellsOrderFieldProps {
+ prefix: 'aspirate' | 'dispense' | 'mix'
+ firstName: string
+ secondName: string
+ updateFirstWellOrder: FieldProps['updateValue']
+ updateSecondWellOrder: FieldProps['updateValue']
+ firstValue?: WellOrderOption | null
+ secondValue?: WellOrderOption | null
+}
+
+export function WellsOrderField(props: WellsOrderFieldProps): JSX.Element {
+ const {
+ firstValue,
+ secondValue,
+ firstName,
+ secondName,
+ prefix,
+ updateFirstWellOrder,
+ updateSecondWellOrder,
+ } = props
+ const { t, i18n } = useTranslation(['form', 'modal', 'protocol_steps'])
+ const [isModalOpen, setModalOpen] = useState(false)
+
+ const handleOpen = (): void => {
+ setModalOpen(true)
+ }
+ const handleClose = (): void => {
+ setModalOpen(false)
+ }
+
+ const updateValues = (firstValue: unknown, secondValue: unknown): void => {
+ updateFirstWellOrder(firstValue)
+ updateSecondWellOrder(secondValue)
+ }
+
+ const [targetProps, tooltipProps] = useHoverTooltip()
+
+ return (
+ <>
+
+ {t('step_edit_form.field.well_order.label')}
+
+
+
+ {i18n.format(
+ t('protocol_steps:well_order_title', { prefix }),
+ 'capitalize'
+ )}
+
+
+
+ {t(`step_edit_form.field.well_order.option.${firstValue}`)}
+ {', '}
+ {t(`step_edit_form.field.well_order.option.${secondValue}`)}
+
+
+
+
+ >
+ )
+}
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/index.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/index.ts
index 36edfa514a4..667ad102c14 100644
--- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/index.ts
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/index.ts
@@ -1,11 +1,17 @@
+export * from './BlowoutLocationField'
+export * from './BlowoutOffsetField'
export * from './ChangeTipField'
+export * from './DisposalField'
export * from './DropTipField'
+export * from './FlowRateField'
export * from './LabwareField'
export * from './PartialTipField'
export * from './PathField'
export * from './PickUpTipField'
export * from './PipetteField'
+export * from './PositionField'
export * from './TiprackField'
export * from './TipWellSelectionField'
export * from './VolumeField'
export * from './WellSelectionField'
+export * from './WellsOrderField'
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx
index fc56a417fd4..f30e5338b60 100644
--- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx
@@ -1,5 +1,14 @@
import { useSelector } from 'react-redux'
-import { DIRECTION_COLUMN, Divider, Flex } from '@opentrons/components'
+import { useTranslation } from 'react-i18next'
+import { useState } from 'react'
+import {
+ DIRECTION_COLUMN,
+ Divider,
+ Flex,
+ SPACING,
+ StyledText,
+ Tabs,
+} from '@opentrons/components'
import { getEnableReturnTip } from '../../../../../../feature-flags/selectors'
import {
getAdditionalEquipmentEntities,
@@ -7,31 +16,64 @@ import {
getPipetteEntities,
} from '../../../../../../step-forms/selectors'
import {
+ CheckboxExpandStepFormField,
+ InputStepFormField,
+} from '../../../../../../molecules'
+import {
+ BlowoutLocationField,
+ BlowoutOffsetField,
ChangeTipField,
+ DisposalField,
DropTipField,
+ FlowRateField,
LabwareField,
PartialTipField,
PathField,
PickUpTipField,
PipetteField,
+ PositionField,
TiprackField,
TipWellSelectionField,
VolumeField,
WellSelectionField,
+ WellsOrderField,
} from '../../PipetteFields'
+import {
+ getBlowoutLocationOptionsForForm,
+ getLabwareFieldForPositioningField,
+} from '../../utils'
+import type { StepFieldName } from '../../../../../../form-types'
import type { StepFormProps } from '../../types'
+const makeAddFieldNamePrefix = (prefix: string) => (
+ fieldName: string
+): StepFieldName => `${prefix}_${fieldName}`
+
export function MoveLiquidTools(props: StepFormProps): JSX.Element {
const { toolboxStep, propsForFields, formData } = props
- // TODO: these will be used for the 2nd page advanced settings
- // const { stepType, path } = formData
+ const { t, i18n } = useTranslation(['protocol_steps', 'form'])
+ const { path } = formData
+ const [tab, setTab] = useState<'aspirate' | 'dispense'>('aspirate')
const additionalEquipmentEntities = useSelector(
getAdditionalEquipmentEntities
)
const enableReturnTip = useSelector(getEnableReturnTip)
const labwares = useSelector(getLabwareEntities)
const pipettes = useSelector(getPipetteEntities)
+ const addFieldNamePrefix = makeAddFieldNamePrefix(tab)
+ const isWasteChuteSelected =
+ propsForFields.dispense_labware?.value != null
+ ? additionalEquipmentEntities[
+ String(propsForFields.dispense_labware.value)
+ ]?.name === 'wasteChute'
+ : false
+ const isTrashBinSelected =
+ propsForFields.dispense_labware?.value != null
+ ? additionalEquipmentEntities[
+ String(propsForFields.dispense_labware.value)
+ ]?.name === 'trashBin'
+ : false
const userSelectedPickUpTipLocation =
labwares[String(propsForFields.pickUpTip_location.value)] != null
const userSelectedDropTipLocation =
@@ -46,6 +88,24 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element {
additionalEquipmentEntities[String(propsForFields.dispense_labware.value)]
?.name === 'trashBin'
+ const aspirateTab = {
+ text: t('aspirate'),
+ isActive: tab === 'aspirate',
+ onClick: () => {
+ setTab('aspirate')
+ },
+ }
+ const dispenseTab = {
+ text: t('dispense'),
+
+ isActive: tab === 'dispense',
+ onClick: () => {
+ setTab('dispense')
+ },
+ }
+ const hideWellOrderField =
+ tab === 'dispense' && (isWasteChuteSelected || isTrashBinSelected)
+
return toolboxStep === 0 ? (
@@ -131,7 +191,243 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element {
) : (
- // TODO: wire up the second page
- wire this up
+
+
+
+
+
+
+
+
+
+ {hideWellOrderField ? null : (
+
+ )}
+
+
+
+
+
+ {t('protocol_steps:advanced_settings')}
+
+ {tab === 'aspirate' ? (
+
+ ) : null}
+
+ {formData[`${tab}_mix_checkbox`] === true ? (
+
+
+
+
+ ) : null}
+
+
+ {formData[`${tab}_delay_checkbox`] === true ? (
+
+
+
+
+ ) : null}
+
+ {tab === 'dispense' ? (
+
+ {formData.blowout_checkbox === true ? (
+
+
+
+
+
+ ) : null}
+
+ ) : null}
+
+ {formData[`${tab}_touchTip_checkbox`] === true ? (
+
+ ) : null}
+
+
+ {formData[`${tab}_airGap_checkbox`] === true ? (
+
+ ) : null}
+
+ {path === 'multiDispense' && tab === 'dispense' && (
+
+ )}
+
+
)
}