diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json
index e992034540d..93892b830b9 100644
--- a/app/src/assets/localization/en/quick_transfer.json
+++ b/app/src/assets/localization/en/quick_transfer.json
@@ -1,8 +1,10 @@
{
"all": "All labware",
+ "aspirate_volume": "Aspirate volume per well (µL)",
"create_new_transfer": "Create new quick transfer",
"left_mount": "Left Mount",
"both_mounts": "Left + Right Mount",
+ "dispense_volume": "Dispense volume per well (µL)",
"right_mount": "Right Mount",
"reservoir": "Reservoirs",
"select_attached_pipette": "Select attached pipette",
@@ -18,6 +20,8 @@
"use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.",
"tip_rack": "Tip rack",
"tubeRack": "Tube racks",
+ "volume_per_well": "Volume per well (µL)",
+ "value_out_of_range": "Value must be between {{min}}-{{max}}",
"labware": "Labware",
"pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.",
"wellPlate": "Well plates",
diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css
index 28fe3159979..3df1453b84c 100644
--- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css
+++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css
@@ -10,6 +10,7 @@
.simple-keyboard.oddTheme1.hg-theme-default {
width: 100%;
height: 100%;
+ border-radius: 0;
background-color: #cbcccc; /* grey35 */
font-family: 'Public Sans', sans-serif;
padding: 8px;
diff --git a/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx b/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx
new file mode 100644
index 00000000000..218d0954a99
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/SelectDestWells.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Flex, SPACING } from '@opentrons/components'
+
+import { SmallButton } from '../../atoms/buttons'
+import { ChildNavigation } from '../ChildNavigation'
+
+import type {
+ QuickTransferSetupState,
+ QuickTransferWizardAction,
+} from './types'
+
+interface SelectDestWellsProps {
+ onNext: () => void
+ onBack: () => void
+ exitButtonProps: React.ComponentProps
+ state: QuickTransferSetupState
+ dispatch: React.Dispatch
+}
+
+export function SelectDestWells(props: SelectDestWellsProps): JSX.Element {
+ const { onNext, onBack, exitButtonProps, state, dispatch } = props
+ const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
+
+ const handleClickNext = (): void => {
+ // until well selection is implemented, select all wells and proceed to the next step
+ if (state.destination === 'source' && state.source != null) {
+ dispatch({
+ type: 'SET_DEST_WELLS',
+ wells: Object.keys(state.source.wells),
+ })
+ } else if (state.destination != 'source' && state.destination != null) {
+ dispatch({
+ type: 'SET_DEST_WELLS',
+ wells: Object.keys(state.destination.wells),
+ })
+ }
+ onNext()
+ }
+ return (
+
+
+
+ TODO: Add destination well selection deck map
+
+
+ )
+}
diff --git a/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx b/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx
new file mode 100644
index 00000000000..1cdc2b75595
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx
@@ -0,0 +1,54 @@
+import * as React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Flex, SPACING } from '@opentrons/components'
+
+import { SmallButton } from '../../atoms/buttons'
+import { ChildNavigation } from '../ChildNavigation'
+
+import type {
+ QuickTransferSetupState,
+ QuickTransferWizardAction,
+} from './types'
+
+interface SelectSourceWellsProps {
+ onNext: () => void
+ onBack: () => void
+ exitButtonProps: React.ComponentProps
+ state: QuickTransferSetupState
+ dispatch: React.Dispatch
+}
+
+export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element {
+ const { onNext, onBack, exitButtonProps, state, dispatch } = props
+ const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
+
+ const handleClickNext = (): void => {
+ // until well selection is implemented, select all wells and proceed to the next step
+ if (state.source?.wells != null) {
+ dispatch({
+ type: 'SET_SOURCE_WELLS',
+ wells: Object.keys(state.source.wells),
+ })
+ onNext()
+ }
+ }
+ return (
+
+
+
+ TODO: Add source well selection deck map
+
+
+ )
+}
diff --git a/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx b/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx
index 98b175b1229..ec5e3cad0e0 100644
--- a/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx
+++ b/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx
@@ -34,7 +34,6 @@ export function SelectTipRack(props: SelectTipRackProps): JSX.Element {
const handleClickNext = (): void => {
// the button will be disabled if this values is null
if (selectedTipRack != null) {
- console.log('submitting tip rack')
dispatch({
type: 'SELECT_TIP_RACK',
tipRack: selectedTipRack,
diff --git a/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx b/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx
new file mode 100644
index 00000000000..84b807eed21
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/VolumeEntry.tsx
@@ -0,0 +1,125 @@
+import * as React from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ Flex,
+ SPACING,
+ DIRECTION_COLUMN,
+ ALIGN_CENTER,
+} from '@opentrons/components'
+
+import { SmallButton } from '../../atoms/buttons'
+import { ChildNavigation } from '../ChildNavigation'
+import { InputField } from '../../atoms/InputField'
+import { NumericalKeyboard } from '../../atoms/SoftwareKeyboard'
+import { getVolumeLimits } from './utils'
+
+import type {
+ QuickTransferSetupState,
+ QuickTransferWizardAction,
+} from './types'
+
+interface VolumeEntryProps {
+ onNext: () => void
+ onBack: () => void
+ exitButtonProps: React.ComponentProps
+ state: QuickTransferSetupState
+ dispatch: React.Dispatch
+}
+
+export function VolumeEntry(props: VolumeEntryProps): JSX.Element {
+ const { onNext, onBack, exitButtonProps, state, dispatch } = props
+ const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
+ const keyboardRef = React.useRef(null)
+
+ const [volume, setVolume] = React.useState('')
+ const volumeRange = getVolumeLimits(state)
+ let headerCopy = t('set_transfer_volume')
+ let textEntryCopy = t('volume_per_well')
+ if (
+ state.sourceWells != null &&
+ state.destinationWells != null &&
+ state.sourceWells.length > state.destinationWells?.length
+ ) {
+ headerCopy = t('set_aspirate_volume')
+ textEntryCopy = t('aspirate_volume')
+ } else if (
+ state.sourceWells != null &&
+ state.destinationWells != null &&
+ state.sourceWells.length < state.destinationWells.length
+ ) {
+ headerCopy = t('set_dispense_volume')
+ textEntryCopy = t('dispense_volume')
+ }
+
+ const volumeAsNumber = Number(volume)
+
+ const handleClickNext = (): void => {
+ // the button will be disabled if this values is null
+ if (volumeAsNumber != null) {
+ dispatch({
+ type: 'SET_VOLUME',
+ volume: volumeAsNumber,
+ })
+ onNext()
+ }
+ }
+
+ const error =
+ volume != '' &&
+ (volumeAsNumber < volumeRange.min || volumeAsNumber > volumeRange.max)
+ ? t(`value_out_of_range`, {
+ min: volumeRange.min,
+ max: volumeRange.max,
+ })
+ : null
+
+ return (
+
+
+
+
+
+
+
+ setVolume(e)}
+ />
+
+
+
+ )
+}
diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx
new file mode 100644
index 00000000000..25e5206ac01
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx
@@ -0,0 +1,102 @@
+import * as React from 'react'
+import { fireEvent, screen } from '@testing-library/react'
+import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'
+
+import { renderWithProviders } from '../../../__testing-utils__'
+import { i18n } from '../../../i18n'
+import { SelectDestLabware } from '../SelectDestLabware'
+
+vi.mock('@opentrons/react-api-client')
+const render = (props: React.ComponentProps) => {
+ return renderWithProviders(, {
+ i18nInstance: i18n,
+ })
+}
+
+describe('SelectDestLabware', () => {
+ let props: React.ComponentProps
+
+ beforeEach(() => {
+ props = {
+ onNext: vi.fn(),
+ onBack: vi.fn(),
+ exitButtonProps: {
+ buttonType: 'tertiaryLowLight',
+ buttonText: 'Exit',
+ onClick: vi.fn(),
+ },
+ state: {
+ mount: 'left',
+ pipette: {
+ channels: 1,
+ } as any,
+ },
+ dispatch: vi.fn(),
+ }
+ })
+ afterEach(() => {
+ vi.resetAllMocks()
+ })
+
+ it('renders the select destination labware screen, header, and exit button', () => {
+ render(props)
+ screen.getByText('Select destination labware')
+ const exitBtn = screen.getByText('Exit')
+ fireEvent.click(exitBtn)
+ expect(props.exitButtonProps.onClick).toHaveBeenCalled()
+ })
+
+ it('renders continue button and it is disabled if no labware is selected', () => {
+ render(props)
+ screen.getByText('Continue')
+ const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
+ expect(continueBtn).toBeDisabled()
+ })
+
+ it('selects labware by default if there is one in state, button will be enabled', () => {
+ render({ ...props, state: { destination: { def: 'definition' } as any } })
+ const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
+ expect(continueBtn).toBeEnabled()
+ fireEvent.click(continueBtn)
+ expect(props.onNext).toHaveBeenCalled()
+ expect(props.dispatch).toHaveBeenCalled()
+ })
+
+ it('renders all categories for a single channel pipette', () => {
+ render(props)
+ screen.getByText('All labware')
+ screen.getByText('Well plates')
+ screen.getByText('Reservoirs')
+ screen.getByText('Tube racks')
+ })
+
+ it.fails('does not render tube rack tab for multi channel pipette', () => {
+ render({ ...props, state: { pipette: { channels: 8 } as any } })
+ screen.getByText('Tube racks')
+ })
+
+ it('renders the source labware as the first option', () => {
+ render({
+ ...props,
+ state: {
+ source: { metadata: { displayName: 'source labware name' } } as any,
+ },
+ })
+ render(props)
+ screen.getByText('Source labware in D2')
+ screen.getByText('source labware name')
+ })
+ it('enables continue button if you select a labware', () => {
+ render({
+ ...props,
+ state: {
+ source: { metadata: { displayName: 'source labware name' } } as any,
+ },
+ })
+ const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
+ expect(continueBtn).toBeDisabled()
+ const sourceLabware = screen.getByText('Source labware in D2')
+ fireEvent.click(sourceLabware)
+ expect(continueBtn).toBeEnabled()
+ })
+})
diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectSourceLabware.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectSourceLabware.test.tsx
new file mode 100644
index 00000000000..c340ef25547
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectSourceLabware.test.tsx
@@ -0,0 +1,77 @@
+import * as React from 'react'
+import { fireEvent, screen } from '@testing-library/react'
+import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'
+
+import { renderWithProviders } from '../../../__testing-utils__'
+import { i18n } from '../../../i18n'
+import { SelectSourceLabware } from '../SelectSourceLabware'
+
+vi.mock('@opentrons/react-api-client')
+const render = (props: React.ComponentProps) => {
+ return renderWithProviders(, {
+ i18nInstance: i18n,
+ })
+}
+
+describe('SelectSourceLabware', () => {
+ let props: React.ComponentProps
+
+ beforeEach(() => {
+ props = {
+ onNext: vi.fn(),
+ onBack: vi.fn(),
+ exitButtonProps: {
+ buttonType: 'tertiaryLowLight',
+ buttonText: 'Exit',
+ onClick: vi.fn(),
+ },
+ state: {
+ mount: 'left',
+ pipette: {
+ channels: 1,
+ } as any,
+ },
+ dispatch: vi.fn(),
+ }
+ })
+ afterEach(() => {
+ vi.resetAllMocks()
+ })
+
+ it('renders the select source labware screen, header, and exit button', () => {
+ render(props)
+ screen.getByText('Select source labware')
+ const exitBtn = screen.getByText('Exit')
+ fireEvent.click(exitBtn)
+ expect(props.exitButtonProps.onClick).toHaveBeenCalled()
+ })
+
+ it('renders continue button and it is disabled if no labware is selected', () => {
+ render(props)
+ screen.getByText('Continue')
+ const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
+ expect(continueBtn).toBeDisabled()
+ })
+
+ it('selects labware by default if there is one in state, button will be enabled', () => {
+ render({ ...props, state: { source: { def: 'definition' } as any } })
+ const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
+ expect(continueBtn).toBeEnabled()
+ fireEvent.click(continueBtn)
+ expect(props.onNext).toHaveBeenCalled()
+ expect(props.dispatch).toHaveBeenCalled()
+ })
+
+ it('renders all categories for a single channel pipette', () => {
+ render(props)
+ screen.getByText('All labware')
+ screen.getByText('Well plates')
+ screen.getByText('Reservoirs')
+ screen.getByText('Tube racks')
+ })
+
+ it.fails('does not render tube rack tab for multi channel pipette', () => {
+ render({ ...props, state: { pipette: { channels: 8 } as any } })
+ screen.getByText('Tube racks')
+ })
+})
diff --git a/app/src/organisms/QuickTransferFlow/__tests__/VolumeEntry.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/VolumeEntry.test.tsx
new file mode 100644
index 00000000000..9da08bbcc08
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/__tests__/VolumeEntry.test.tsx
@@ -0,0 +1,116 @@
+import * as React from 'react'
+import { fireEvent, screen } from '@testing-library/react'
+import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'
+
+import { renderWithProviders } from '../../../__testing-utils__'
+import { i18n } from '../../../i18n'
+import { InputField } from '../../../atoms/InputField'
+import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard'
+import { VolumeEntry } from '../VolumeEntry'
+
+vi.mock('../../../atoms/InputField')
+vi.mock('../../../atoms/SoftwareKeyboard')
+
+const render = (props: React.ComponentProps) => {
+ return renderWithProviders(, {
+ i18nInstance: i18n,
+ })
+}
+
+describe('VolumeEntry', () => {
+ let props: React.ComponentProps
+
+ beforeEach(() => {
+ props = {
+ onNext: vi.fn(),
+ onBack: vi.fn(),
+ exitButtonProps: {
+ buttonType: 'tertiaryLowLight',
+ buttonText: 'Exit',
+ onClick: vi.fn(),
+ },
+ state: {
+ mount: 'left',
+ pipette: {
+ channels: 1,
+ } as any,
+ sourceWells: ['A1'],
+ destinationWells: ['A1'],
+ },
+ dispatch: vi.fn(),
+ }
+ })
+ afterEach(() => {
+ vi.resetAllMocks()
+ })
+
+ it('renders the volume entry screen, continue, and exit buttons', () => {
+ render(props)
+ const exitBtn = screen.getByText('Exit')
+ fireEvent.click(exitBtn)
+ expect(props.exitButtonProps.onClick).toHaveBeenCalled()
+ expect(vi.mocked(InputField)).toHaveBeenCalled()
+ expect(vi.mocked(NumericalKeyboard)).toHaveBeenCalled()
+ const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
+ expect(continueBtn).toBeDisabled()
+ })
+
+ it('renders transfer text if there are more destination wells than source wells', () => {
+ render(props)
+ screen.getByText('Set transfer volume')
+ expect(vi.mocked(InputField)).toHaveBeenCalledWith(
+ {
+ title: 'Volume per well (µL)',
+ error: null,
+ readOnly: true,
+ type: 'text',
+ value: '',
+ },
+ {}
+ )
+ })
+
+ it('renders dispense text if there are more destination wells than source wells', () => {
+ render({
+ ...props,
+ state: {
+ sourceWells: ['A1'],
+ destinationWells: ['A1', 'A2'],
+ },
+ })
+ render(props)
+ screen.getByText('Set dispense volume')
+ expect(vi.mocked(InputField)).toHaveBeenCalledWith(
+ {
+ title: 'Dispense volume per well (µL)',
+ error: null,
+ readOnly: true,
+ type: 'text',
+ value: '',
+ },
+ {}
+ )
+ })
+
+ it('renders aspirate text if there are more destination wells than source wells', () => {
+ render({
+ ...props,
+ state: {
+ sourceWells: ['A1', 'A2'],
+ destinationWells: ['A1'],
+ },
+ })
+ render(props)
+ screen.getByText('Set aspirate volume')
+ expect(vi.mocked(InputField)).toHaveBeenCalledWith(
+ {
+ title: 'Aspirate volume per well (µL)',
+ error: null,
+ readOnly: true,
+ type: 'text',
+ value: '',
+ },
+ {}
+ )
+ })
+})
diff --git a/app/src/organisms/QuickTransferFlow/__tests__/utils.test.ts b/app/src/organisms/QuickTransferFlow/__tests__/utils.test.ts
new file mode 100644
index 00000000000..063a810daa7
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/__tests__/utils.test.ts
@@ -0,0 +1,80 @@
+import { describe, it, expect } from 'vitest'
+import { getVolumeLimits } from '../utils'
+
+import type { QuickTransferSetupState } from '../types'
+
+describe('getVolumeLimits', () => {
+ let state: QuickTransferSetupState = {
+ pipette: {
+ liquids: [
+ {
+ maxVolume: 1000,
+ minVolume: 5,
+ },
+ ] as any,
+ } as any,
+ tipRack: {
+ wells: {
+ A1: {
+ totalLiquidVolume: 200,
+ },
+ } as any,
+ } as any,
+ source: {
+ wells: {
+ A1: {
+ totalLiquidVolume: 200,
+ },
+ A2: {
+ totalLiquidVolume: 75,
+ },
+ A3: {
+ totalLiquidVolume: 100,
+ },
+ } as any,
+ } as any,
+ sourceWells: ['A1'],
+ destination: {
+ wells: {
+ A1: {
+ totalLiquidVolume: 1000,
+ },
+ A2: {
+ totalLiquidVolume: 1000,
+ },
+ } as any,
+ } as any,
+ destinationWells: ['A1'],
+ }
+ it('calculates the range for a 1 to 1 transfer', () => {
+ const result = getVolumeLimits(state)
+ expect(result.min).toEqual(5)
+ // should equal lesser of pipette max, tip capacity, volume of all selected wells
+ expect(result.max).toEqual(200)
+ })
+ it('calculates the range for an n to 1 transfer', () => {
+ const result = getVolumeLimits({ ...state, sourceWells: ['A1', 'A2'] })
+ expect(result.min).toEqual(5)
+ // should equal lesser of pipette max, tip capacity, volume of all
+ // selected source wells and 1 / 2 volume of destination well
+ expect(result.max).toEqual(75)
+ })
+ it('calculates the range for an 1 to n transfer', () => {
+ const result = getVolumeLimits({ ...state, destinationWells: ['A1', 'A2'] })
+ expect(result.min).toEqual(5)
+ // should equal lesser of pipette max, tip capacity, volume of all
+ // selected destination wells and 1 / 2 volume of source well
+ expect(result.max).toEqual(100)
+ })
+ it('calculates the range for 1 to n transfer with same labware', () => {
+ const result = getVolumeLimits({
+ ...state,
+ destination: 'source',
+ destinationWells: ['A2', 'A3'],
+ })
+ expect(result.min).toEqual(5)
+ // should equal lesser of pipette max, tip capacity, volume of all
+ // selected destination wells and 1 / 2 volume of source well
+ expect(result.max).toEqual(75)
+ })
+})
diff --git a/app/src/organisms/QuickTransferFlow/index.tsx b/app/src/organisms/QuickTransferFlow/index.tsx
index 262156d28df..804acf36a95 100644
--- a/app/src/organisms/QuickTransferFlow/index.tsx
+++ b/app/src/organisms/QuickTransferFlow/index.tsx
@@ -1,19 +1,16 @@
import * as React from 'react'
import { useHistory } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
-import {
- Flex,
- StepMeter,
- SPACING,
- POSITION_STICKY,
-} from '@opentrons/components'
+import { StepMeter, POSITION_STICKY } from '@opentrons/components'
import { SmallButton } from '../../atoms/buttons'
-import { ChildNavigation } from '../ChildNavigation'
import { CreateNewTransfer } from './CreateNewTransfer'
import { SelectPipette } from './SelectPipette'
import { SelectTipRack } from './SelectTipRack'
import { SelectSourceLabware } from './SelectSourceLabware'
+import { SelectSourceWells } from './SelectSourceWells'
import { SelectDestLabware } from './SelectDestLabware'
+import { SelectDestWells } from './SelectDestWells'
+import { VolumeEntry } from './VolumeEntry'
import { quickTransferReducer } from './utils'
import type { QuickTransferSetupState } from './types'
@@ -29,11 +26,6 @@ export const QuickTransferFlow = (): JSX.Element => {
initialQuickTransferState
)
const [currentStep, setCurrentStep] = React.useState(1)
- const [continueIsDisabled] = React.useState(false)
-
- // every child component will take state as a prop, an anonymous
- // dispatch function related to that step (except create new),
- // and a function to disable the continue button
const exitButtonProps: React.ComponentProps = {
buttonType: 'tertiaryLowLight',
@@ -43,19 +35,15 @@ export const QuickTransferFlow = (): JSX.Element => {
},
}
- // these will be moved to the child components once they all exist
- const ORDERED_STEP_HEADERS: string[] = [
- t('create_new_transfer'),
- t('select_attached_pipette'),
- t('select_tip_rack'),
- t('select_source_labware'),
- t('select_source_wells'),
- t('select_dest_labware'),
- t('select_dest_wells'),
- t('set_transfer_volume'),
- ]
+ React.useEffect(() => {
+ if (state.volume != null) {
+ // until summary screen is implemented, log the final state and close flow
+ // once volume is set
+ console.log('final quick transfer flow state:', state)
+ history.push('protocols')
+ }
+ }, [state.volume])
- const header = ORDERED_STEP_HEADERS[currentStep - 1]
let modalContent: JSX.Element | null = null
if (currentStep === 1) {
modalContent = (
@@ -94,6 +82,16 @@ export const QuickTransferFlow = (): JSX.Element => {
exitButtonProps={exitButtonProps}
/>
)
+ } else if (currentStep === 5) {
+ modalContent = (
+ setCurrentStep(prevStep => prevStep - 1)}
+ onNext={() => setCurrentStep(prevStep => prevStep + 1)}
+ exitButtonProps={exitButtonProps}
+ />
+ )
} else if (currentStep === 6) {
modalContent = (
{
exitButtonProps={exitButtonProps}
/>
)
+ } else if (currentStep === 7) {
+ modalContent = (
+ setCurrentStep(prevStep => prevStep - 1)}
+ onNext={() => setCurrentStep(prevStep => prevStep + 1)}
+ exitButtonProps={exitButtonProps}
+ />
+ )
+ } else if (currentStep === 8) {
+ modalContent = (
+ setCurrentStep(prevStep => prevStep - 1)}
+ onNext={() => {}}
+ exitButtonProps={exitButtonProps}
+ />
+ )
} else {
modalContent = null
}
- // until each page is wired up, show header title with empty screen
return (
<>
{
position={POSITION_STICKY}
top="0"
/>
- {modalContent == null ? (
-
- {
- setCurrentStep(prevStep => prevStep - 1)
- }
- }
- buttonText={i18n.format(t('shared:continue'), 'capitalize')}
- onClickButton={() => {
- if (currentStep === 8) {
- history.push('protocols')
- } else {
- setCurrentStep(prevStep => prevStep + 1)
- }
- }}
- buttonIsDisabled={continueIsDisabled}
- secondaryButtonProps={{
- buttonType: 'tertiaryLowLight',
- buttonText: i18n.format(t('shared:exit'), 'capitalize'),
- onClick: () => {
- history.push('protocols')
- },
- }}
- top={SPACING.spacing8}
- />
- {modalContent}
-
- ) : (
- modalContent
- )}
+ {modalContent}
>
)
}
diff --git a/app/src/organisms/QuickTransferFlow/utils.ts b/app/src/organisms/QuickTransferFlow/utils.ts
index 4d1f7633a16..647faa598e1 100644
--- a/app/src/organisms/QuickTransferFlow/utils.ts
+++ b/app/src/organisms/QuickTransferFlow/utils.ts
@@ -118,3 +118,72 @@ export function getCompatibleLabwareByCategory(
)
}
}
+
+export function getVolumeLimits(
+ state: QuickTransferSetupState
+): { min: number; max: number } {
+ if (
+ state.pipette == null ||
+ state.tipRack == null ||
+ state.source == null ||
+ state.sourceWells == null ||
+ state.destination == null ||
+ state.destinationWells == null
+ ) {
+ // this should only be called once all state values are set
+ return { min: 0, max: 0 }
+ }
+
+ const minPipetteVolume = Object.values(state.pipette.liquids)[0].minVolume
+ const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume
+ const tipRackVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume
+ const sourceLabwareVolume = Math.min(
+ ...state.sourceWells.map(well => state.source.wells[well].totalLiquidVolume)
+ )
+
+ const destLabwareVolume = Math.min(
+ ...state.destinationWells.map(well =>
+ state.destination === 'source'
+ ? state.source.wells[well].totalLiquidVolume
+ : state.destination.wells[well].totalLiquidVolume
+ )
+ )
+ let maxVolume = maxPipetteVolume
+ if (state.sourceWells.length === state.destinationWells.length) {
+ // 1 to 1 transfer
+ maxVolume = Math.min(
+ ...[
+ maxPipetteVolume,
+ tipRackVolume,
+ sourceLabwareVolume,
+ destLabwareVolume,
+ ]
+ )
+ } else if (state.sourceWells.length < state.destinationWells.length) {
+ // 1 to n transfer
+ const ratio = state.sourceWells.length / state.destinationWells.length
+
+ maxVolume = Math.min(
+ ...[
+ maxPipetteVolume,
+ tipRackVolume,
+ sourceLabwareVolume * ratio,
+ destLabwareVolume,
+ ]
+ )
+ } else if (state.sourceWells.length > state.destinationWells.length) {
+ // n to 1 transfer
+ const ratio = state.destinationWells.length / state.sourceWells.length
+
+ maxVolume = Math.min(
+ ...[
+ maxPipetteVolume,
+ tipRackVolume,
+ sourceLabwareVolume,
+ destLabwareVolume * ratio,
+ ]
+ )
+ }
+
+ return { min: minPipetteVolume, max: maxVolume }
+}