Skip to content

Commit

Permalink
feat(app): add tip management tab and items (#15291)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
3 people authored Jun 5, 2024
1 parent 38f6bd1 commit f49c33d
Show file tree
Hide file tree
Showing 18 changed files with 901 additions and 119 deletions.
11 changes: 11 additions & 0 deletions app/src/assets/localization/en/quick_transfer.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"advanced_settings": "Advanced settings",
"all": "All labware",
"always": "Before every aspirate",
"aspirate_volume": "Aspirate volume per well",
"aspirate_volume_µL": "Aspirate volume per well (µL)",
"both_mounts": "Left + Right Mount",
"change_tip": "Change tip",
"create_new_transfer": "Create new quick transfer",
"create_transfer": "Create transfer",
"destination": "Destination",
Expand All @@ -13,13 +15,17 @@
"exit_quick_transfer": "Exit quick transfer?",
"left_mount": "Left Mount",
"lose_all_progress": "You will lose all progress on this quick transfer.",
"once": "Once at the start of the transfer",
"overview": "Overview",
"perDest": "Per destination well",
"perSource": "Per source well",
"pipette": "Pipette",
"quick_transfer_volume": "Quick Transfer {{volume}}µL",
"right_mount": "Right Mount",
"reservoir": "Reservoirs",
"run_now": "Run now",
"run_quick_transfer_now": "Do you want to run your quick transfer now?",
"save": "Save",
"save_to_run_later": "Save your quick transfer to run it in the future.",
"save_for_later": "Save for later",
"source": "Source",
Expand All @@ -35,14 +41,19 @@
"source_labware": "Source labware",
"source_labware_d2": "Source labware in D2",
"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>",
"tip_drop_location": "Tip drop location",
"tip_management": "Tip management",
"tip_rack": "Tip rack",
"trashBin": "Trash bin",
"trashBin_location": "Trash bin in {{slotName}}",
"tubeRack": "Tube racks",
"volume_per_well": "Volume per well",
"volume_per_well_µL": "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.",
"wasteChute": "Waste chute",
"wasteChute_location": "Waste chute in {{slotName}}",
"wellPlate": "Well plates",
"well_selection": "Well selection",
"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)."
Expand Down
11 changes: 9 additions & 2 deletions app/src/atoms/ListItem/__tests__/ListItem.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react'
import { describe, it, expect, beforeEach } from 'vitest'
import { vi, describe, it, expect, beforeEach } from 'vitest'
import '@testing-library/jest-dom/vitest'
import { screen } from '@testing-library/react'
import { fireEvent, screen } from '@testing-library/react'
import { BORDERS, COLORS, SPACING } from '@opentrons/components'
import { renderWithProviders } from '../../../__testing-utils__'

Expand All @@ -17,6 +17,7 @@ describe('ListItem', () => {
props = {
type: 'error',
children: <div>mock listitem content</div>,
onClick: vi.fn(),
}
})

Expand Down Expand Up @@ -63,4 +64,10 @@ describe('ListItem', () => {
)
expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius12}`)
})
it('should call on click when pressed', () => {
render(props)
const listItem = screen.getByText('mock listitem content')
fireEvent.click(listItem)
expect(props.onClick).toHaveBeenCalled()
})
})
4 changes: 3 additions & 1 deletion app/src/atoms/ListItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ListItemProps extends StyleProps {
type: ListItemType
/** ListItem contents */
children: React.ReactNode
onClick?: () => void
}

const LISTITEM_PROPS_BY_TYPE: Record<
Expand All @@ -32,7 +33,7 @@ const LISTITEM_PROPS_BY_TYPE: Record<
}

export function ListItem(props: ListItemProps): JSX.Element {
const { type, children, ...styleProps } = props
const { type, children, onClick, ...styleProps } = props
const listItemProps = LISTITEM_PROPS_BY_TYPE[type]

return (
Expand All @@ -43,6 +44,7 @@ export function ListItem(props: ListItemProps): JSX.Element {
padding={`${SPACING.spacing16} ${SPACING.spacing24}`}
backgroundColor={listItemProps.backgroundColor}
borderRadius={BORDERS.borderRadius12}
onClick={onClick}
{...styleProps}
>
{children}
Expand Down
18 changes: 12 additions & 6 deletions app/src/organisms/QuickTransferFlow/SummaryAndSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configurat
import { TabbedButton } from '../../atoms/buttons'
import { ChildNavigation } from '../ChildNavigation'
import { Overview } from './Overview'
import { TipManagement } from './TipManagement'
import { SaveOrRunModal } from './SaveOrRunModal'
import { getInitialSummaryState } from './utils'
import { createQuickTransferFile } from './utils/createQuickTransferFile'
import { getInitialSummaryState, createQuickTransferFile } from './utils'
import { quickTransferSummaryReducer } from './reducers'

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

// @ts-expect-error TODO figure out how to make this type non-null as we know
// none of these values will be undefined
const initialSummaryState = getInitialSummaryState(wizardFlowState)
const [state] = React.useReducer(
const initialSummaryState = getInitialSummaryState({
// @ts-expect-error TODO figure out how to make this type non-null as we know
// none of these values will be undefined
state: wizardFlowState,
deckConfig,
})
const [state, dispatch] = React.useReducer(
quickTransferSummaryReducer,
initialSummaryState
)
Expand Down Expand Up @@ -135,6 +138,9 @@ export function SummaryAndSettings(
))}
</Flex>
{selectedCategory === 'overview' ? <Overview state={state} /> : null}
{selectedCategory === 'tip_management' ? (
<TipManagement state={state} dispatch={dispatch} />
) : null}
</Flex>
</Flex>
)
Expand Down
87 changes: 87 additions & 0 deletions app/src/organisms/QuickTransferFlow/TipManagement/ChangeTip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { createPortal } from 'react-dom'
import {
Flex,
SPACING,
DIRECTION_COLUMN,
POSITION_FIXED,
COLORS,
} from '@opentrons/components'
import { getTopPortalEl } from '../../../App/portal'
import { LargeButton } from '../../../atoms/buttons'
import { ChildNavigation } from '../../ChildNavigation'

import type {
ChangeTipOptions,
QuickTransferSummaryState,
QuickTransferSummaryAction,
} from '../types'

interface ChangeTipProps {
onBack: () => void
state: QuickTransferSummaryState
dispatch: React.Dispatch<QuickTransferSummaryAction>
}

export function ChangeTip(props: ChangeTipProps): JSX.Element {
const { onBack, state, dispatch } = props
const { t } = useTranslation('quick_transfer')

const allowedChangeTipOptions: ChangeTipOptions[] = ['once']
if (state.sourceWells.length <= 96 && state.destinationWells.length <= 96) {
allowedChangeTipOptions.push('always')
}
if (state.path === 'single' && state.transferType === 'distribute') {
allowedChangeTipOptions.push('perDest')
} else if (state.path === 'single') {
allowedChangeTipOptions.push('perSource')
}

const [
selectedChangeTipOption,
setSelectedChangeTipOption,
] = React.useState<ChangeTipOptions>(state.changeTip)

const handleClickSave = (): void => {
if (selectedChangeTipOption !== state.changeTip) {
dispatch({
type: 'SET_CHANGE_TIP',
changeTip: selectedChangeTipOption,
})
}
onBack()
}
return createPortal(
<Flex position={POSITION_FIXED} backgroundColor={COLORS.white} width="100%">
<ChildNavigation
header={t('change_tip')}
buttonText={t('save')}
onClickBack={onBack}
onClickButton={handleClickSave}
buttonIsDisabled={selectedChangeTipOption == null}
/>
<Flex
marginTop={SPACING.spacing120}
flexDirection={DIRECTION_COLUMN}
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
gridGap={SPACING.spacing4}
width="100%"
>
{allowedChangeTipOptions.map(option => (
<LargeButton
key={option}
buttonType={
selectedChangeTipOption === option ? 'primary' : 'secondary'
}
onClick={() => {
setSelectedChangeTipOption(option)
}}
buttonText={t(`${option}`)}
/>
))}
</Flex>
</Flex>,
getTopPortalEl()
)
}
109 changes: 109 additions & 0 deletions app/src/organisms/QuickTransferFlow/TipManagement/TipDropLocation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { createPortal } from 'react-dom'
import {
Flex,
SPACING,
DIRECTION_COLUMN,
POSITION_FIXED,
COLORS,
} from '@opentrons/components'
import {
WASTE_CHUTE_FIXTURES,
FLEX_SINGLE_SLOT_BY_CUTOUT_ID,
TRASH_BIN_ADAPTER_FIXTURE,
} from '@opentrons/shared-data'
import { getTopPortalEl } from '../../../App/portal'
import { LargeButton } from '../../../atoms/buttons'
import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration'
import { ChildNavigation } from '../../ChildNavigation'

import type {
QuickTransferSummaryState,
QuickTransferSummaryAction,
} from '../types'
import type { CutoutConfig } from '@opentrons/shared-data'

interface TipDropLocationProps {
onBack: () => void
state: QuickTransferSummaryState
dispatch: React.Dispatch<QuickTransferSummaryAction>
}

export function TipDropLocation(props: TipDropLocationProps): JSX.Element {
const { onBack, state, dispatch } = props
const { t } = useTranslation('quick_transfer')
const deckConfig = useNotifyDeckConfigurationQuery().data ?? []

const tipDropLocationOptions = deckConfig.filter(
cutoutConfig =>
WASTE_CHUTE_FIXTURES.includes(cutoutConfig.cutoutFixtureId) ||
TRASH_BIN_ADAPTER_FIXTURE === cutoutConfig.cutoutFixtureId
)

// add trash bin in A3 if no trash or waste chute configured
if (tipDropLocationOptions.length === 0) {
tipDropLocationOptions.push({
cutoutId: 'cutoutA3',
cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
})
}

const [
selectedTipDropLocation,
setSelectedTipDropLocation,
] = React.useState<CutoutConfig>(state.dropTipLocation)

const handleClickSave = (): void => {
if (selectedTipDropLocation.cutoutId !== state.dropTipLocation.cutoutId) {
dispatch({
type: 'SET_DROP_TIP_LOCATION',
location: selectedTipDropLocation,
})
}
onBack()
}
return createPortal(
<Flex position={POSITION_FIXED} backgroundColor={COLORS.white} width="100%">
<ChildNavigation
header={t('tip_drop_location')}
buttonText={t('save')}
onClickBack={onBack}
onClickButton={handleClickSave}
buttonIsDisabled={selectedTipDropLocation == null}
/>
<Flex
marginTop={SPACING.spacing120}
flexDirection={DIRECTION_COLUMN}
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
gridGap={SPACING.spacing4}
width="100%"
>
{tipDropLocationOptions.map(option => (
<LargeButton
key={option.cutoutId}
buttonType={
selectedTipDropLocation.cutoutId === option.cutoutId
? 'primary'
: 'secondary'
}
onClick={() => {
setSelectedTipDropLocation(option)
}}
buttonText={t(
`${
option.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE
? 'trashBin'
: 'wasteChute'
}_location`,
{
slotName: FLEX_SINGLE_SLOT_BY_CUTOUT_ID[option.cutoutId],
}
)}
/>
))}
</Flex>
</Flex>,
getTopPortalEl()
)
}
Loading

0 comments on commit f49c33d

Please sign in to comment.