diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index 5bb266367e5..99e7bb0d6ad 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -7,6 +7,7 @@ "air_gap": "Air gap", "air_gap_before_aspirating": "Air gap before aspirating", "air_gap_before_dispensing": "Air gap before dispensing", + "air_gap_capacity_error": "The tip is too full to add an air gap.", "air_gap_value": "{{volume}} µL", "air_gap_volume_µL": "Air gap volume (µL)", "all": "All labware", @@ -33,6 +34,8 @@ "character_limit_error": "Character limit exceeded", "column": "column", "columns": "columns", + "consolidate_volume_error": "The selected destination well is too small to consolidate into. Try consolidating from fewer wells.", + "create_new_to_edit": "Create a new quick transfer to edit", "create_new_transfer": "Create new quick transfer", "create_transfer": "Create transfer", "delay": "Delay", @@ -56,6 +59,7 @@ "dispense_volume_µL": "Dispense volume per well (µL)", "disposal_volume_µL": "Disposal volume (µL)", "distance_bottom_of_well_mm": "Distance from bottom of well (mm)", + "distribute_volume_error": "The selected source well is too small to distribute from. Try distributing to fewer wells.", "enter_characters": "Enter up to 60 characters", "error_analyzing": "An error occurred while attempting to analyze {{transferName}}.", "exit_quick_transfer": "Exit quick transfer?", diff --git a/app/src/organisms/QuickTransferFlow/Overview.tsx b/app/src/organisms/QuickTransferFlow/Overview.tsx index a71bf2004f6..685bac69333 100644 --- a/app/src/organisms/QuickTransferFlow/Overview.tsx +++ b/app/src/organisms/QuickTransferFlow/Overview.tsx @@ -10,6 +10,7 @@ import { COLORS, TEXT_ALIGN_RIGHT, } from '@opentrons/components' +import { useToaster } from '../ToasterOven' import { ListItem } from '../../atoms/ListItem' import { CONSOLIDATE, DISTRIBUTE } from './constants' @@ -22,6 +23,7 @@ interface OverviewProps { export function Overview(props: OverviewProps): JSX.Element | null { const { state } = props const { t } = useTranslation(['quick_transfer', 'shared']) + const { makeSnackbar } = useToaster() let transferCopy = t('volume_per_well') if (state.transferType === CONSOLIDATE) { @@ -29,6 +31,9 @@ export function Overview(props: OverviewProps): JSX.Element | null { } else if (state.transferType === DISTRIBUTE) { transferCopy = t('dispense_volume') } + const onClick = (): void => { + makeSnackbar(t('create_new_to_edit') as string) + } const displayItems = [ { @@ -63,7 +68,7 @@ export function Overview(props: OverviewProps): JSX.Element | null { marginTop="192px" > {displayItems.map(displayItem => ( - + volumeRange.max) - ? t(`value_out_of_range`, { - min: volumeRange.min, - max: volumeRange.max, - }) - : null + let volumeError = null + if (volumeRange.min > volumeRange.max) { + volumeError = t('air_gap_capacity_error') + } else if ( + volume !== null && + (volume < volumeRange.min || volume > volumeRange.max) + ) { + volumeError = t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + } let buttonIsDisabled = false if (currentStep === 2) { diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx index b93cee1a9d7..d42fa76ade7 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx @@ -14,7 +14,6 @@ import { getTopPortalEl } from '../../../App/portal' import { LargeButton } from '../../../atoms/buttons' import { ChildNavigation } from '../../ChildNavigation' import { useBlowOutLocationOptions } from './BlowOut' -import { getVolumeRange } from '../utils' import type { PathOption, @@ -48,7 +47,11 @@ export function PipettePath(props: PipettePathProps): JSX.Element { const [disposalVolume, setDisposalVolume] = React.useState( state.volume ) - const volumeLimits = getVolumeRange(state) + const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume + const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume + + // this is the max amount of liquid that can be held in the tip at any time + const maxTipCapacity = Math.min(maxPipetteVolume, tipVolume) const allowedPipettePathOptions: Array<{ pathOption: PathOption @@ -56,7 +59,7 @@ export function PipettePath(props: PipettePathProps): JSX.Element { }> = [{ pathOption: 'single', description: t('pipette_path_single') }] if ( state.transferType === 'distribute' && - volumeLimits.max >= state.volume * 3 + maxTipCapacity >= state.volume * 3 ) { // we have the capacity for a multi dispense if we can fit at least 2x the volume per well // for aspiration plus 1x the volume per well for disposal volume @@ -67,7 +70,7 @@ export function PipettePath(props: PipettePathProps): JSX.Element { // for multi aspirate we only need at least 2x the volume per well } else if ( state.transferType === 'consolidate' && - volumeLimits.max >= state.volume * 2 + maxTipCapacity >= state.volume * 2 ) { allowedPipettePathOptions.push({ pathOption: 'multiAspirate', @@ -113,8 +116,8 @@ export function PipettePath(props: PipettePathProps): JSX.Element { ? t('shared:continue') : t('shared:save') - const maxVolumeCapacity = volumeLimits.max - state.volume * 2 - const volumeRange = { min: 1, max: maxVolumeCapacity } + const maxDisposalCapacity = maxTipCapacity - state.volume * 2 + const volumeRange = { min: 1, max: maxDisposalCapacity } const volumeError = disposalVolume !== null && diff --git a/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx b/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx index e1214d49045..6cfb03a5ed7 100644 --- a/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx +++ b/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx @@ -58,15 +58,21 @@ export function VolumeEntry(props: VolumeEntryProps): JSX.Element { onNext() } } - - const error = + let error = null + if (volumeRange.min > volumeRange.max) { + error = + state.transferType === 'consolidate' + ? t('consolidate_volume_error') + : t('distribute_volume_error') + } else if ( volume !== '' && (volumeAsNumber < volumeRange.min || volumeAsNumber > volumeRange.max) - ? t(`value_out_of_range`, { - min: volumeRange.min, - max: volumeRange.max, - }) - : null + ) { + error = t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + } return ( diff --git a/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts b/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts index 4937af941d5..0f04ae374c0 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts +++ b/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts @@ -1,8 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, vi, afterEach } from 'vitest' import { getInitialSummaryState } from '../../utils' -import { getVolumeRange } from '../../utils/getVolumeRange' - -vi.mock('../../utils/getVolumeRange') describe('getInitialSummaryState', () => { const props = { @@ -11,6 +8,7 @@ describe('getInitialSummaryState', () => { channels: 1, liquids: { default: { + maxVolume: 100, supportedTips: { t50: { defaultAspirateFlowRate: { @@ -46,9 +44,6 @@ describe('getInitialSummaryState', () => { }, ], } as any - beforeEach(() => { - vi.mocked(getVolumeRange).mockReturnValue({ min: 5, max: 100 }) - }) afterEach(() => { vi.resetAllMocks() }) @@ -125,11 +120,13 @@ describe('getInitialSummaryState', () => { ...props, state: { ...props.state, + volume: 10, transferType: 'distribute', }, }) expect(initialSummaryState).toEqual({ ...props.state, + volume: 10, transferType: 'distribute', aspirateFlowRate: 50, dispenseFlowRate: 75, @@ -142,7 +139,7 @@ describe('getInitialSummaryState', () => { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter', }, - disposalVolume: props.state.volume, + disposalVolume: 10, blowOut: { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter' }, }) }) diff --git a/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts b/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts index d073dd13894..cb6187a388c 100644 --- a/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts +++ b/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts @@ -3,7 +3,6 @@ import { TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_FIXTURES, } from '@opentrons/shared-data' -import { getVolumeRange } from './' import type { LabwareDefinition2, @@ -43,20 +42,24 @@ export function getInitialSummaryState( const flowRatesForSupportedTip = state.pipette.liquids.default.supportedTips[tipType] - const volumeLimits = getVolumeRange(state) + const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume + const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume + + // this is the max amount of liquid that can be held in the tip at any time + const maxTipCapacity = Math.min(maxPipetteVolume, tipVolume) let path: PathOption = 'single' // for multiDispense the volume capacity must be at least 3x the volume per well // to account for the 1x volume per well disposal volume default if ( state.transferType === 'distribute' && - volumeLimits.max >= state.volume * 3 + maxTipCapacity >= state.volume * 3 ) { path = 'multiDispense' // for multiAspirate the volume capacity must be at least 2x the volume per well } else if ( state.transferType === 'consolidate' && - volumeLimits.max >= state.volume * 2 + maxTipCapacity >= state.volume * 2 ) { path = 'multiAspirate' }