diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 6adfae63960..dfc3e0c6345 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -1,19 +1,29 @@ { "add_details": "Add step details", + "advanced_settings": "Advanced pipetting settings", + "air_gap_volume": "Air gap volume", + "aspirate": "Aspirate", "aspirated": "Aspirated", "batch_edit_steps": "Batch edit steps", "batch_edit": "Batch edit", "batch_edits_saved": "Batch edits saved", + "blowout_location": "Blowout location", + "blowout_position": "Blowout position from bottom", "change_tips": "Change tips", "default_tip_option": "Default - get next tip", + "delay_duration": "Delay duration", + "delay_position": "Delay position from bottom", "delete_steps": "Delete steps", "delete": "Delete step", + "dispense": "Dispense", "dispensed": "Dispensed", + "disposal_volume": "Disposal volume", "duplicate_steps": "Duplicate steps", "duplicate": "Duplicate step", "edit_step": "Edit step", "engage_height": "Engage height", "final_deck_state": "Final deck state", + "flow_type_title": "{{type}} flow rate", "from": "from", "heater_shaker": { "active": { @@ -35,6 +45,9 @@ "disengage": "{{module}}disengaged", "engage": "{{module}}engaged to" }, + "max_disposal_volume": "Max {{vol}} {{unit}}", + "mix_times": "Mix repititions", + "mix_volume": "Mix volume", "mix": "Mix", "mix_step": "Mixing{{times}} times in{{labware}}", "mix_repetitions": "Mix repetitions", @@ -48,6 +61,7 @@ "distribute": "Distributingfrom{{source}}to{{destination}}", "transfer": "Transferringfrom{{source}}to{{destination}}" }, + "multi_dispense_options": "Distribute options", "multiAspirate": "Consolidate path", "multiDispense": "Distribute path", "new_location": "New location", @@ -105,6 +119,11 @@ } }, "time": "Time", + "tip_position": "{{prefix}} tip position", + "touch_tip_position": "Touch tip position from top", + "valid_range": "Valid range between {{min}} - {{max}} {{unit}}", "view_details": "View details", - "well_name": "Well {{wellName}}" + "well_name": "Well {{wellName}}", + "well_order_title": "{{prefix}} well order", + "well_position": "Well position (x,y,z): " } diff --git a/protocol-designer/src/assets/localization/en/shared.json b/protocol-designer/src/assets/localization/en/shared.json index e6456184334..51525de47d6 100644 --- a/protocol-designer/src/assets/localization/en/shared.json +++ b/protocol-designer/src/assets/localization/en/shared.json @@ -14,6 +14,7 @@ "destination_well": "Destination Well", "developer_ff": "Developer feature flags", "done": "Done", + "pipette": "Pipette", "edit_existing": "Edit existing protocol", "edit_instruments": "Edit Instruments", "edit_pipette": "Edit Pipette", diff --git a/protocol-designer/src/molecules/CheckboxExpandStepFormField/index.tsx b/protocol-designer/src/molecules/CheckboxExpandStepFormField/index.tsx index 06db7033790..d9cdc22b6c8 100644 --- a/protocol-designer/src/molecules/CheckboxExpandStepFormField/index.tsx +++ b/protocol-designer/src/molecules/CheckboxExpandStepFormField/index.tsx @@ -16,7 +16,7 @@ interface CheckboxExpandStepFormFieldProps { checkboxUpdateValue: (value: unknown) => 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 b6940d38fc6..a6777a5be00 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 ( - + 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' && ( + + )} + + ) }