diff --git a/components/src/atoms/InputField/index.tsx b/components/src/atoms/InputField/index.tsx index 8a9eb76c2d8e..8933ca5345c1 100644 --- a/components/src/atoms/InputField/index.tsx +++ b/components/src/atoms/InputField/index.tsx @@ -234,6 +234,7 @@ export const InputField = React.forwardRef( return ( void checkboxValue: unknown isChecked: boolean - children: React.ReactNode + children?: React.ReactNode } export function CheckboxExpandStepFormField( props: CheckboxExpandStepFormFieldProps diff --git a/protocol-designer/src/molecules/DropdownStepFormField/index.tsx b/protocol-designer/src/molecules/DropdownStepFormField/index.tsx index b6940d38fc6d..a6777a5be006 100644 --- a/protocol-designer/src/molecules/DropdownStepFormField/index.tsx +++ b/protocol-designer/src/molecules/DropdownStepFormField/index.tsx @@ -6,6 +6,8 @@ import type { FieldProps } from '../../pages/Designer/ProtocolSteps/StepForm/typ export interface DropdownStepFormFieldProps extends FieldProps { options: Options title: string + addPadding?: boolean + width?: string } export function DropdownStepFormField( @@ -18,15 +20,17 @@ export function DropdownStepFormField( title, errorToShow, tooltipContent, + addPadding = true, + width = '17.5rem', } = props const { t } = useTranslation('tooltip') const availableOptionId = options.find(opt => opt.value === value) return ( - + { + 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/BlowoutZOffsetField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/BlowoutZOffsetField.tsx new file mode 100644 index 000000000000..ee9a7830c768 --- /dev/null +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/BlowoutZOffsetField.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 BlowoutZOffsetFieldProps extends FieldProps { + destLabwareId: unknown + sourceLabwareId?: unknown + blowoutLabwareId?: unknown +} + +export function BlowoutZOffsetField( + props: BlowoutZOffsetFieldProps +): 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/DisposalVolumeField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalVolumeField.tsx new file mode 100644 index 000000000000..6409cad7bb4f --- /dev/null +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalVolumeField.tsx @@ -0,0 +1,117 @@ +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 { BlowoutZOffsetField } from './BlowoutZOffsetField' + +import type { PathOption, StepType } from '../../../../../form-types' +import type { FieldPropsByName } from '../types' + +interface DisposalVolumeFieldProps { + 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 const DisposalVolumeField = ( + props: DisposalVolumeFieldProps +): 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 000000000000..a89c4f0be62f --- /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 000000000000..70513e45783b --- /dev/null +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateInput.tsx @@ -0,0 +1,254 @@ +import * as React 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 { 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 const 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] = React.useState< + InitialState['isPristine'] + >(initialState.isPristine) + + const [modalFlowRate, setModalFlowRate] = React.useState< + InitialState['modalFlowRate'] + >(initialState.modalFlowRate) + + const [modalUseDefault, setModalUseDefault] = React.useState< + InitialState['modalUseDefault'] + >(initialState.modalUseDefault) + + const [showModal, setShowModal] = React.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: React.ChangeEvent): void => { + setModalUseDefault(e.target.value !== 'custom') + } + + const handleChangeNumber = (e: React.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/TipPositionField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TipPositionField.tsx new file mode 100644 index 000000000000..6662df503e03 --- /dev/null +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TipPositionField.tsx @@ -0,0 +1,212 @@ +import * as React 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 TipPositionFieldProps { + prefix: 'aspirate' | 'dispense' + propsForFields: FieldPropsByName + zField: TipZOffsetFields + xField?: TipXOffsetFields + yField?: TipYOffsetFields + labwareId?: string | null +} + +export function TipPositionField(props: TipPositionFieldProps): 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] = React.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/WellOrderField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellOrderField.tsx new file mode 100644 index 000000000000..7e396df9e1e8 --- /dev/null +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellOrderField.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 WellOrderFieldProps { + prefix: 'aspirate' | 'dispense' | 'mix' + firstName: string + secondName: string + updateFirstWellOrder: FieldProps['updateValue'] + updateSecondWellOrder: FieldProps['updateValue'] + firstValue?: WellOrderOption | null + secondValue?: WellOrderOption | null +} + +export const WellOrderField = (props: WellOrderFieldProps): 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 36edfa514a47..70c760fa490e 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 './BlowoutZOffsetField' export * from './ChangeTipField' +export * from './DisposalVolumeField' export * from './DropTipField' +export * from './FlowRateField' export * from './LabwareField' export * from './PartialTipField' export * from './PathField' export * from './PickUpTipField' export * from './PipetteField' +export * from './TipPositionField' export * from './TiprackField' export * from './TipWellSelectionField' export * from './VolumeField' +export * from './WellOrderField' export * from './WellSelectionField' 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 fc56a417fd48..316b6dec0eb5 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, + BlowoutZOffsetField, ChangeTipField, + DisposalVolumeField, DropTipField, + FlowRateField, LabwareField, PartialTipField, PathField, PickUpTipField, PipetteField, + TipPositionField, TiprackField, TipWellSelectionField, VolumeField, + WellOrderField, WellSelectionField, } 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' && ( + + )} + + ) }