Skip to content

Commit f49c33d

Browse files
smb2268b-cooperbrenthagen
authored
feat(app): add tip management tab and items (#15291)
This PR adds the tip management tab with change tip and drop tip location settings. fix PLAT-224 Co-authored-by: Brian Cooper <[email protected]> Co-authored-by: Brent Hagen <[email protected]>
1 parent 38f6bd1 commit f49c33d

18 files changed

+901
-119
lines changed

app/src/assets/localization/en/quick_transfer.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
{
22
"advanced_settings": "Advanced settings",
33
"all": "All labware",
4+
"always": "Before every aspirate",
45
"aspirate_volume": "Aspirate volume per well",
56
"aspirate_volume_µL": "Aspirate volume per well (µL)",
67
"both_mounts": "Left + Right Mount",
8+
"change_tip": "Change tip",
79
"create_new_transfer": "Create new quick transfer",
810
"create_transfer": "Create transfer",
911
"destination": "Destination",
@@ -13,13 +15,17 @@
1315
"exit_quick_transfer": "Exit quick transfer?",
1416
"left_mount": "Left Mount",
1517
"lose_all_progress": "You will lose all progress on this quick transfer.",
18+
"once": "Once at the start of the transfer",
1619
"overview": "Overview",
20+
"perDest": "Per destination well",
21+
"perSource": "Per source well",
1722
"pipette": "Pipette",
1823
"quick_transfer_volume": "Quick Transfer {{volume}}µL",
1924
"right_mount": "Right Mount",
2025
"reservoir": "Reservoirs",
2126
"run_now": "Run now",
2227
"run_quick_transfer_now": "Do you want to run your quick transfer now?",
28+
"save": "Save",
2329
"save_to_run_later": "Save your quick transfer to run it in the future.",
2430
"save_for_later": "Save for later",
2531
"source": "Source",
@@ -35,14 +41,19 @@
3541
"source_labware": "Source labware",
3642
"source_labware_d2": "Source labware in D2",
3743
"use_deck_slots": "<block>Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.</block><block>Make sure that your deck configuration is up to date to avoid collisions.</block>",
44+
"tip_drop_location": "Tip drop location",
3845
"tip_management": "Tip management",
3946
"tip_rack": "Tip rack",
47+
"trashBin": "Trash bin",
48+
"trashBin_location": "Trash bin in {{slotName}}",
4049
"tubeRack": "Tube racks",
4150
"volume_per_well": "Volume per well",
4251
"volume_per_well_µL": "Volume per well (µL)",
4352
"value_out_of_range": "Value must be between {{min}}-{{max}}",
4453
"labware": "Labware",
4554
"pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.",
55+
"wasteChute": "Waste chute",
56+
"wasteChute_location": "Waste chute in {{slotName}}",
4657
"wellPlate": "Well plates",
4758
"well_selection": "Well selection",
4859
"well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well)."

app/src/atoms/ListItem/__tests__/ListItem.test.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react'
2-
import { describe, it, expect, beforeEach } from 'vitest'
2+
import { vi, describe, it, expect, beforeEach } from 'vitest'
33
import '@testing-library/jest-dom/vitest'
4-
import { screen } from '@testing-library/react'
4+
import { fireEvent, screen } from '@testing-library/react'
55
import { BORDERS, COLORS, SPACING } from '@opentrons/components'
66
import { renderWithProviders } from '../../../__testing-utils__'
77

@@ -17,6 +17,7 @@ describe('ListItem', () => {
1717
props = {
1818
type: 'error',
1919
children: <div>mock listitem content</div>,
20+
onClick: vi.fn(),
2021
}
2122
})
2223

@@ -63,4 +64,10 @@ describe('ListItem', () => {
6364
)
6465
expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius12}`)
6566
})
67+
it('should call on click when pressed', () => {
68+
render(props)
69+
const listItem = screen.getByText('mock listitem content')
70+
fireEvent.click(listItem)
71+
expect(props.onClick).toHaveBeenCalled()
72+
})
6673
})

app/src/atoms/ListItem/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface ListItemProps extends StyleProps {
1111
type: ListItemType
1212
/** ListItem contents */
1313
children: React.ReactNode
14+
onClick?: () => void
1415
}
1516

1617
const LISTITEM_PROPS_BY_TYPE: Record<
@@ -32,7 +33,7 @@ const LISTITEM_PROPS_BY_TYPE: Record<
3233
}
3334

3435
export function ListItem(props: ListItemProps): JSX.Element {
35-
const { type, children, ...styleProps } = props
36+
const { type, children, onClick, ...styleProps } = props
3637
const listItemProps = LISTITEM_PROPS_BY_TYPE[type]
3738

3839
return (
@@ -43,6 +44,7 @@ export function ListItem(props: ListItemProps): JSX.Element {
4344
padding={`${SPACING.spacing16} ${SPACING.spacing24}`}
4445
backgroundColor={listItemProps.backgroundColor}
4546
borderRadius={BORDERS.borderRadius12}
47+
onClick={onClick}
4648
{...styleProps}
4749
>
4850
{children}

app/src/organisms/QuickTransferFlow/SummaryAndSettings.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configurat
2121
import { TabbedButton } from '../../atoms/buttons'
2222
import { ChildNavigation } from '../ChildNavigation'
2323
import { Overview } from './Overview'
24+
import { TipManagement } from './TipManagement'
2425
import { SaveOrRunModal } from './SaveOrRunModal'
25-
import { getInitialSummaryState } from './utils'
26-
import { createQuickTransferFile } from './utils/createQuickTransferFile'
26+
import { getInitialSummaryState, createQuickTransferFile } from './utils'
2727
import { quickTransferSummaryReducer } from './reducers'
2828

2929
import type { SmallButton } from '../../atoms/buttons'
@@ -56,10 +56,13 @@ export function SummaryAndSettings(
5656
)
5757
const deckConfig = useNotifyDeckConfigurationQuery().data ?? []
5858

59-
// @ts-expect-error TODO figure out how to make this type non-null as we know
60-
// none of these values will be undefined
61-
const initialSummaryState = getInitialSummaryState(wizardFlowState)
62-
const [state] = React.useReducer(
59+
const initialSummaryState = getInitialSummaryState({
60+
// @ts-expect-error TODO figure out how to make this type non-null as we know
61+
// none of these values will be undefined
62+
state: wizardFlowState,
63+
deckConfig,
64+
})
65+
const [state, dispatch] = React.useReducer(
6366
quickTransferSummaryReducer,
6467
initialSummaryState
6568
)
@@ -135,6 +138,9 @@ export function SummaryAndSettings(
135138
))}
136139
</Flex>
137140
{selectedCategory === 'overview' ? <Overview state={state} /> : null}
141+
{selectedCategory === 'tip_management' ? (
142+
<TipManagement state={state} dispatch={dispatch} />
143+
) : null}
138144
</Flex>
139145
</Flex>
140146
)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as React from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { createPortal } from 'react-dom'
4+
import {
5+
Flex,
6+
SPACING,
7+
DIRECTION_COLUMN,
8+
POSITION_FIXED,
9+
COLORS,
10+
} from '@opentrons/components'
11+
import { getTopPortalEl } from '../../../App/portal'
12+
import { LargeButton } from '../../../atoms/buttons'
13+
import { ChildNavigation } from '../../ChildNavigation'
14+
15+
import type {
16+
ChangeTipOptions,
17+
QuickTransferSummaryState,
18+
QuickTransferSummaryAction,
19+
} from '../types'
20+
21+
interface ChangeTipProps {
22+
onBack: () => void
23+
state: QuickTransferSummaryState
24+
dispatch: React.Dispatch<QuickTransferSummaryAction>
25+
}
26+
27+
export function ChangeTip(props: ChangeTipProps): JSX.Element {
28+
const { onBack, state, dispatch } = props
29+
const { t } = useTranslation('quick_transfer')
30+
31+
const allowedChangeTipOptions: ChangeTipOptions[] = ['once']
32+
if (state.sourceWells.length <= 96 && state.destinationWells.length <= 96) {
33+
allowedChangeTipOptions.push('always')
34+
}
35+
if (state.path === 'single' && state.transferType === 'distribute') {
36+
allowedChangeTipOptions.push('perDest')
37+
} else if (state.path === 'single') {
38+
allowedChangeTipOptions.push('perSource')
39+
}
40+
41+
const [
42+
selectedChangeTipOption,
43+
setSelectedChangeTipOption,
44+
] = React.useState<ChangeTipOptions>(state.changeTip)
45+
46+
const handleClickSave = (): void => {
47+
if (selectedChangeTipOption !== state.changeTip) {
48+
dispatch({
49+
type: 'SET_CHANGE_TIP',
50+
changeTip: selectedChangeTipOption,
51+
})
52+
}
53+
onBack()
54+
}
55+
return createPortal(
56+
<Flex position={POSITION_FIXED} backgroundColor={COLORS.white} width="100%">
57+
<ChildNavigation
58+
header={t('change_tip')}
59+
buttonText={t('save')}
60+
onClickBack={onBack}
61+
onClickButton={handleClickSave}
62+
buttonIsDisabled={selectedChangeTipOption == null}
63+
/>
64+
<Flex
65+
marginTop={SPACING.spacing120}
66+
flexDirection={DIRECTION_COLUMN}
67+
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
68+
gridGap={SPACING.spacing4}
69+
width="100%"
70+
>
71+
{allowedChangeTipOptions.map(option => (
72+
<LargeButton
73+
key={option}
74+
buttonType={
75+
selectedChangeTipOption === option ? 'primary' : 'secondary'
76+
}
77+
onClick={() => {
78+
setSelectedChangeTipOption(option)
79+
}}
80+
buttonText={t(`${option}`)}
81+
/>
82+
))}
83+
</Flex>
84+
</Flex>,
85+
getTopPortalEl()
86+
)
87+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as React from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { createPortal } from 'react-dom'
4+
import {
5+
Flex,
6+
SPACING,
7+
DIRECTION_COLUMN,
8+
POSITION_FIXED,
9+
COLORS,
10+
} from '@opentrons/components'
11+
import {
12+
WASTE_CHUTE_FIXTURES,
13+
FLEX_SINGLE_SLOT_BY_CUTOUT_ID,
14+
TRASH_BIN_ADAPTER_FIXTURE,
15+
} from '@opentrons/shared-data'
16+
import { getTopPortalEl } from '../../../App/portal'
17+
import { LargeButton } from '../../../atoms/buttons'
18+
import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration'
19+
import { ChildNavigation } from '../../ChildNavigation'
20+
21+
import type {
22+
QuickTransferSummaryState,
23+
QuickTransferSummaryAction,
24+
} from '../types'
25+
import type { CutoutConfig } from '@opentrons/shared-data'
26+
27+
interface TipDropLocationProps {
28+
onBack: () => void
29+
state: QuickTransferSummaryState
30+
dispatch: React.Dispatch<QuickTransferSummaryAction>
31+
}
32+
33+
export function TipDropLocation(props: TipDropLocationProps): JSX.Element {
34+
const { onBack, state, dispatch } = props
35+
const { t } = useTranslation('quick_transfer')
36+
const deckConfig = useNotifyDeckConfigurationQuery().data ?? []
37+
38+
const tipDropLocationOptions = deckConfig.filter(
39+
cutoutConfig =>
40+
WASTE_CHUTE_FIXTURES.includes(cutoutConfig.cutoutFixtureId) ||
41+
TRASH_BIN_ADAPTER_FIXTURE === cutoutConfig.cutoutFixtureId
42+
)
43+
44+
// add trash bin in A3 if no trash or waste chute configured
45+
if (tipDropLocationOptions.length === 0) {
46+
tipDropLocationOptions.push({
47+
cutoutId: 'cutoutA3',
48+
cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
49+
})
50+
}
51+
52+
const [
53+
selectedTipDropLocation,
54+
setSelectedTipDropLocation,
55+
] = React.useState<CutoutConfig>(state.dropTipLocation)
56+
57+
const handleClickSave = (): void => {
58+
if (selectedTipDropLocation.cutoutId !== state.dropTipLocation.cutoutId) {
59+
dispatch({
60+
type: 'SET_DROP_TIP_LOCATION',
61+
location: selectedTipDropLocation,
62+
})
63+
}
64+
onBack()
65+
}
66+
return createPortal(
67+
<Flex position={POSITION_FIXED} backgroundColor={COLORS.white} width="100%">
68+
<ChildNavigation
69+
header={t('tip_drop_location')}
70+
buttonText={t('save')}
71+
onClickBack={onBack}
72+
onClickButton={handleClickSave}
73+
buttonIsDisabled={selectedTipDropLocation == null}
74+
/>
75+
<Flex
76+
marginTop={SPACING.spacing120}
77+
flexDirection={DIRECTION_COLUMN}
78+
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
79+
gridGap={SPACING.spacing4}
80+
width="100%"
81+
>
82+
{tipDropLocationOptions.map(option => (
83+
<LargeButton
84+
key={option.cutoutId}
85+
buttonType={
86+
selectedTipDropLocation.cutoutId === option.cutoutId
87+
? 'primary'
88+
: 'secondary'
89+
}
90+
onClick={() => {
91+
setSelectedTipDropLocation(option)
92+
}}
93+
buttonText={t(
94+
`${
95+
option.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE
96+
? 'trashBin'
97+
: 'wasteChute'
98+
}_location`,
99+
{
100+
slotName: FLEX_SINGLE_SLOT_BY_CUTOUT_ID[option.cutoutId],
101+
}
102+
)}
103+
/>
104+
))}
105+
</Flex>
106+
</Flex>,
107+
getTopPortalEl()
108+
)
109+
}

0 commit comments

Comments
 (0)