Skip to content

Commit 58619d3

Browse files
authored
feat(app): add tip rack to DropTipLocation as a new option (#19897)
* feat(app): add tip rack to DropTipLocation as a new option
1 parent 9b25c77 commit 58619d3

File tree

10 files changed

+214
-66
lines changed

10 files changed

+214
-66
lines changed

app/src/organisms/ODD/QuickTransferFlow/Overview.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useToaster } from '/app/organisms/ToasterOven'
2121

2222
import { CONSOLIDATE, DISTRIBUTE } from './constants'
2323

24+
import type { CutoutConfig } from '@opentrons/shared-data'
2425
import type { QuickTransferSummaryState } from './types'
2526

2627
interface OverviewProps {
@@ -49,6 +50,22 @@ export function Overview(props: OverviewProps): JSX.Element | null {
4950
}
5051
const pathCopy = pathCopyMap[state.path]
5152

53+
const dropTipLocationCopy = (location: CutoutConfig | string): string => {
54+
if (location != null && typeof location !== 'string') {
55+
return t(
56+
`${
57+
location.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE
58+
? 'trashBin'
59+
: 'wasteChute'
60+
}_location`,
61+
{
62+
slotName: FLEX_SINGLE_SLOT_BY_CUTOUT_ID[location.cutoutId],
63+
}
64+
)
65+
}
66+
return t('tip_rack')
67+
}
68+
5269
const displayItems = [
5370
{
5471
option: t('pipette'),
@@ -83,17 +100,7 @@ export function Overview(props: OverviewProps): JSX.Element | null {
83100
},
84101
{
85102
option: t('tip_drop_location'),
86-
value: t(
87-
`${
88-
state.dropTipLocation?.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE
89-
? 'trashBin'
90-
: 'wasteChute'
91-
}_location`,
92-
{
93-
slotName:
94-
FLEX_SINGLE_SLOT_BY_CUTOUT_ID[state.dropTipLocation?.cutoutId],
95-
}
96-
),
103+
value: dropTipLocationCopy(state.dropTipLocation),
97104
},
98105
{
99106
option: t('liquid_class'),

app/src/organisms/ODD/QuickTransferFlow/SelectTipDropLocation.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@opentrons/components'
1010
import {
1111
FLEX_SINGLE_SLOT_BY_CUTOUT_ID,
12+
getLabwareDefURI,
1213
TRASH_BIN_ADAPTER_FIXTURE,
1314
WASTE_CHUTE_FIXTURES,
1415
} from '@opentrons/shared-data'
@@ -41,8 +42,10 @@ export function SelectTipDropLocation({
4142
const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
4243
const deckConfig = useNotifyDeckConfigurationQuery().data ?? []
4344
const [selectedTipDropLocation, setSelectedTipDropLocation] = useState<
44-
CutoutConfig | undefined
45-
>()
45+
CutoutConfig | string | undefined
46+
>(undefined)
47+
48+
const isTipRackReturnEnabled = state.tipRack != null && state.pipette != null
4649

4750
const tipDropLocationOptions = deckConfig.filter(
4851
cutoutConfig =>
@@ -56,6 +59,12 @@ export function SelectTipDropLocation({
5659
})
5760
}
5861

62+
const handleSelectTipRack = (): void => {
63+
if (state.tipRack != null) {
64+
setSelectedTipDropLocation(getLabwareDefURI(state.tipRack))
65+
}
66+
}
67+
5968
const handleClickNext = (): void => {
6069
dispatch({
6170
type: 'SET_DROP_TIP_LOCATION',
@@ -88,7 +97,11 @@ export function SelectTipDropLocation({
8897
{tipDropLocationOptions.map(option => (
8998
<RadioButton
9099
key={option.cutoutId}
91-
isSelected={selectedTipDropLocation?.cutoutId === option.cutoutId}
100+
isSelected={
101+
selectedTipDropLocation != null &&
102+
typeof selectedTipDropLocation !== 'string' &&
103+
selectedTipDropLocation?.cutoutId === option.cutoutId
104+
}
92105
onChange={() => {
93106
setSelectedTipDropLocation(option)
94107
}}
@@ -105,6 +118,19 @@ export function SelectTipDropLocation({
105118
)}
106119
/>
107120
))}
121+
{isTipRackReturnEnabled && state.tipRack != null ? (
122+
<RadioButton
123+
key="tiprack"
124+
isSelected={
125+
selectedTipDropLocation != null &&
126+
typeof selectedTipDropLocation === 'string' &&
127+
selectedTipDropLocation === getLabwareDefURI(state.tipRack)
128+
}
129+
buttonLabel={t('tip_rack')}
130+
buttonValue={getLabwareDefURI(state.tipRack)}
131+
onChange={handleSelectTipRack}
132+
/>
133+
) : null}
108134
</Flex>
109135
</Flex>
110136
)

app/src/organisms/ODD/QuickTransferFlow/__tests__/Overview.test.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { screen } from '@testing-library/react'
2-
import { afterEach, beforeEach, describe, it, vi } from 'vitest'
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
33

4-
import { WATER_LIQUID_CLASS_NAME } from '@opentrons/shared-data'
4+
import {
5+
TRASH_BIN_ADAPTER_FIXTURE,
6+
WATER_LIQUID_CLASS_NAME,
7+
} from '@opentrons/shared-data'
58

69
import { renderWithProviders } from '/app/__testing-utils__'
710
import { i18n } from '/app/i18n'
@@ -56,14 +59,19 @@ describe('Overview', () => {
5659
render(props)
5760
screen.getByText('Pipette')
5861
screen.getByText('Pipette display name')
59-
screen.getByText('Tip rack')
6062
screen.getByText('Tip rack display name')
6163
screen.getByText('Source labware')
6264
screen.getByText('Source labware name')
6365
screen.getByText('Destination labware')
6466
screen.getByText('Destination labware name')
6567
screen.getByText('Volume per well')
6668
screen.getByText('25µL')
69+
screen.getByText('Pipette path')
70+
screen.getByText('Tip change frequency')
71+
screen.getByText('Tip drop location')
72+
screen.getByText('Liquid class')
73+
// one is for item title, one is for the value of tip drop location
74+
expect(screen.getAllByText('Tip rack')).toHaveLength(2)
6775
})
6876
it('renders the correct volume wording for n to 1 transfer', () => {
6977
props = {
@@ -116,13 +124,46 @@ describe('Overview', () => {
116124
} as any,
117125
transferType: 'distribute',
118126
volume: 25,
127+
dropTipLocation: {
128+
cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
129+
cutoutId: 'cutoutA3',
130+
},
119131
} as any,
120132
}
121133
render(props)
122134
screen.getByText('Dispense volume per well')
123135
})
124136

125137
it('should render correct items when liquid classes are enabled', () => {
138+
props = {
139+
state: {
140+
pipette: {
141+
displayName: 'Pipette display name',
142+
} as any,
143+
tipRack: {
144+
metadata: {
145+
displayName: 'Tip rack display name',
146+
},
147+
} as any,
148+
source: {
149+
metadata: {
150+
displayName: 'Source labware name',
151+
},
152+
} as any,
153+
destination: {
154+
metadata: {
155+
displayName: 'Destination labware name',
156+
},
157+
} as any,
158+
transferType: 'distribute',
159+
volume: 25,
160+
dropTipLocation: {
161+
cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
162+
cutoutId: 'cutoutA3',
163+
},
164+
liquidClassName: WATER_LIQUID_CLASS_NAME,
165+
} as any,
166+
}
126167
render(props)
127168
screen.getByText('Pipette')
128169
screen.getByText('Pipette display name')
@@ -135,6 +176,7 @@ describe('Overview', () => {
135176
screen.getByText('Pipette path')
136177
screen.getByText('Tip change frequency')
137178
screen.getByText('Tip drop location')
179+
screen.getByText('Trash bin in A3')
138180
screen.getByText('Liquid class')
139181
screen.getByText('Aqueous')
140182
})

app/src/organisms/ODD/QuickTransferFlow/__tests__/SelectTipDropLocation.test.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { fireEvent, screen } from '@testing-library/react'
22
import { beforeEach, describe, expect, it, vi } from 'vitest'
33

4-
import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data'
4+
import {
5+
getLabwareDefURI,
6+
TRASH_BIN_ADAPTER_FIXTURE,
7+
} from '@opentrons/shared-data'
58

69
import { renderWithProviders } from '/app/__testing-utils__'
710
import { i18n } from '/app/i18n'
811
import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration'
912

13+
import mockQuickTransferState from '../QuickTransferAdvancedSettings/__fixtures__/QuickTransferState.json'
1014
import { SelectTipDropLocation } from '../SelectTipDropLocation'
1115

1216
import type { ComponentProps } from 'react'
17+
import type { LabwareDefinition2 } from '@opentrons/shared-data'
18+
import type { QuickTransferWizardState } from '../types'
1319

1420
vi.mock('/app/resources/deck_configuration')
1521

@@ -42,7 +48,7 @@ describe('SelectTipDropLocation', () => {
4248
buttonText: 'Exit',
4349
onClick: vi.fn(),
4450
},
45-
state: {},
51+
state: mockQuickTransferState as QuickTransferWizardState,
4652
dispatch: vi.fn(),
4753
}
4854
vi.mocked(useNotifyDeckConfigurationQuery).mockReturnValue({
@@ -57,6 +63,7 @@ describe('SelectTipDropLocation', () => {
5763
screen.getByText('Continue')
5864
screen.getByText('Trash bin in A3')
5965
screen.getByText('Waste chute in C3')
66+
screen.getByText('Tip rack')
6067
})
6168

6269
it('should call mock function when tappin exit button', () => {
@@ -65,7 +72,7 @@ describe('SelectTipDropLocation', () => {
6572
expect(props.exitButtonProps.onClick).toHaveBeenCalled()
6673
})
6774

68-
it('should call mock function when tappin continue button', () => {
75+
it('should call mock function when tapping trash bin option and continue button', () => {
6976
render(props)
7077
fireEvent.click(screen.getByText('Trash bin in A3'))
7178
fireEvent.click(screen.getByText('Continue'))
@@ -78,4 +85,17 @@ describe('SelectTipDropLocation', () => {
7885
},
7986
})
8087
})
88+
89+
it('should call mock function when tapping tip rack option and continue button', () => {
90+
render(props)
91+
fireEvent.click(screen.getByText('Tip rack'))
92+
fireEvent.click(screen.getByText('Continue'))
93+
expect(props.onNext).toHaveBeenCalled()
94+
expect(props.dispatch).toHaveBeenCalledWith({
95+
type: 'SET_DROP_TIP_LOCATION',
96+
location: getLabwareDefURI(
97+
mockQuickTransferState.tipRack as LabwareDefinition2
98+
),
99+
})
100+
})
81101
})

app/src/organisms/ODD/QuickTransferFlow/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface QuickTransferWizardState {
2727
// Note added for liquid classes in Quick Transfer
2828
path?: PathOption
2929
changeTip?: ChangeTipOptions
30-
dropTipLocation?: CutoutConfig
30+
dropTipLocation?: CutoutConfig | string
3131
liquidClassName?: string
3232
}
3333
export type PathOption = 'single' | 'multiAspirate' | 'multiDispense'
@@ -120,7 +120,7 @@ export interface QuickTransferSummaryState {
120120
}
121121
airGapDispense?: number
122122
changeTip: ChangeTipOptions
123-
dropTipLocation: CutoutConfig
123+
dropTipLocation: CutoutConfig | string
124124
liquidClassName: string
125125
conditionAspirate?: number
126126
disposalVolumeDispenseSettings?: {
@@ -290,7 +290,7 @@ interface SetChangeTip {
290290
}
291291
interface SetDropTipLocation {
292292
type: typeof ACTIONS.SET_DROP_TIP_LOCATION
293-
location: CutoutConfig
293+
location: CutoutConfig | string
294294
}
295295

296296
interface SetLiquidClassAction {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data'
2+
import {
3+
DEST_WELL_BLOWOUT_DESTINATION,
4+
SOURCE_WELL_BLOWOUT_DESTINATION,
5+
} from '@opentrons/step-generation'
6+
7+
import type { CutoutConfig } from '@opentrons/shared-data'
8+
import type { BlowOutLocation } from '../types'
9+
10+
export const convertBlowoutLocation = (
11+
location: string | undefined,
12+
dropTipLocation: CutoutConfig | string
13+
): BlowOutLocation | undefined => {
14+
if (location == null) return undefined
15+
16+
switch (location) {
17+
case 'source':
18+
return SOURCE_WELL_BLOWOUT_DESTINATION
19+
case 'destination':
20+
return DEST_WELL_BLOWOUT_DESTINATION
21+
case 'trash':
22+
return typeof dropTipLocation !== 'string' &&
23+
'cutoutId' in dropTipLocation
24+
? dropTipLocation
25+
: {
26+
cutoutId: 'cutoutA3',
27+
cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
28+
}
29+
default:
30+
return undefined
31+
}
32+
}

app/src/organisms/ODD/QuickTransferFlow/utils/generateQuickTransferArgs.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,9 @@ export function getInvariantContextAndRobotState(
176176
let wasteChuteEntities: WasteChuteEntities = {}
177177

178178
if (
179+
typeof quickTransferState.dropTipLocation !== 'string' &&
179180
quickTransferState.dropTipLocation.cutoutFixtureId ===
180-
TRASH_BIN_ADAPTER_FIXTURE
181+
TRASH_BIN_ADAPTER_FIXTURE
181182
) {
182183
const trashLocation = quickTransferState.dropTipLocation.cutoutId
183184
const trashId = `${uuid()}_trashBin`
@@ -214,6 +215,7 @@ export function getInvariantContextAndRobotState(
214215
}
215216

216217
if (
218+
typeof quickTransferState.dropTipLocation !== 'string' &&
217219
WASTE_CHUTE_FIXTURES.includes(
218220
quickTransferState.dropTipLocation.cutoutFixtureId
219221
)
@@ -339,20 +341,37 @@ export function generateQuickTransferArgs(
339341
const dropTipTrashBinLocationEntity = Object.values(
340342
invariantContext.trashBinEntities
341343
).find(
342-
entity => entity.location === quickTransferState.dropTipLocation.cutoutId
344+
entity =>
345+
typeof quickTransferState.dropTipLocation !== 'string' &&
346+
entity.location === quickTransferState.dropTipLocation.cutoutId
343347
)
344348
const dropTipWasteChuteLocationEntity = Object.values(
345349
invariantContext.wasteChuteEntities
346350
).find(
347-
entity => entity.location === quickTransferState.dropTipLocation.cutoutId
351+
entity =>
352+
typeof quickTransferState.dropTipLocation !== 'string' &&
353+
entity.location === quickTransferState.dropTipLocation.cutoutId
348354
)
349-
const dropTipLocation =
350-
dropTipTrashBinLocationEntity?.id ??
351-
dropTipWasteChuteLocationEntity?.id ??
352-
''
353355

354-
const pipetteEntity = Object.values(invariantContext.pipetteEntities)[0]
356+
const dropTipIsTiprack =
357+
typeof quickTransferState.dropTipLocation === 'string' &&
358+
quickTransferState.dropTipLocation ===
359+
getLabwareDefURI(quickTransferState.tipRack)
355360

361+
const dropTipLocation = (() => {
362+
if (dropTipIsTiprack) {
363+
return quickTransferState.dropTipLocation as string
364+
}
365+
if (dropTipTrashBinLocationEntity?.id != null) {
366+
return dropTipTrashBinLocationEntity.id
367+
}
368+
if (dropTipWasteChuteLocationEntity?.id != null) {
369+
return dropTipWasteChuteLocationEntity.id
370+
}
371+
return ''
372+
})()
373+
374+
const pipetteEntity = Object.values(invariantContext.pipetteEntities)[0]
356375
const sourceLabwareId = Object.keys(robotState.labware).find(
357376
labwareId =>
358377
getSlotInLocationStack(robotState.labware[labwareId].stack) === 'C2'

0 commit comments

Comments
 (0)