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'
}