Skip to content

Commit

Permalink
feat(app): add labware selection and volume entry screens (#15074)
Browse files Browse the repository at this point in the history
  • Loading branch information
smb2268 authored and Carlos-fernandez committed Jun 3, 2024
1 parent ab6b8a4 commit 99445fb
Show file tree
Hide file tree
Showing 18 changed files with 1,492 additions and 94 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,8 +1,14 @@
{
"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)",
"exit_quick_transfer": "Exit quick transfer?",
"lose_all_progress": "You will lose all progress on this quick transfer.",
"right_mount": "Right Mount",
"reservoir": "Reservoirs",
"select_attached_pipette": "Select attached pipette",
"select_dest_labware": "Select destination labware",
"select_dest_wells": "Select destination wells",
Expand All @@ -12,10 +18,15 @@
"set_aspirate_volume": "Set aspirate volume",
"set_dispense_volume": "Set dispense volume",
"set_transfer_volume": "Set transfer volume",
"source_labware": "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_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",
"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)."
}
1 change: 1 addition & 0 deletions app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
54 changes: 54 additions & 0 deletions app/src/organisms/QuickTransferFlow/ConfirmExitModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import {
SPACING,
COLORS,
StyledText,
Flex,
DIRECTION_COLUMN,
TYPOGRAPHY,
} from '@opentrons/components'
import { Modal } from '../../molecules/Modal'
import { SmallButton } from '../../atoms/buttons'

interface ConfirmExitModalProps {
confirmExit: () => void
cancelExit: () => void
}

export const ConfirmExitModal = (props: ConfirmExitModalProps): JSX.Element => {
const { i18n, t } = useTranslation(['quick_transfer', 'shared'])

return (
<Modal
header={{
title: t('exit_quick_transfer'),
iconName: 'alert-circle',
iconColor: COLORS.yellow50,
}}
>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing10}
width="100%"
>
<StyledText css={TYPOGRAPHY.bodyTextRegular}>
{t('lose_all_progress')}
</StyledText>
<Flex gridGap={SPACING.spacing8}>
<SmallButton
width="50%"
buttonText={i18n.format(t('shared:cancel'), 'capitalize')}
onClick={props.cancelExit}
/>
<SmallButton
width="50%"
buttonText={i18n.format(t('shared:delete'), 'capitalize')}
onClick={props.confirmExit}
buttonType="alert"
/>
</Flex>
</Flex>
</Modal>
)
}
147 changes: 147 additions & 0 deletions app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import {
Flex,
SPACING,
DIRECTION_COLUMN,
DIRECTION_ROW,
COLORS,
POSITION_FIXED,
ALIGN_CENTER,
} from '@opentrons/components'

import { SmallButton, LargeButton, TabbedButton } from '../../atoms/buttons'
import { ChildNavigation } from '../ChildNavigation'
import { getCompatibleLabwareByCategory } from './utils'

import type { LabwareDefinition2 } from '@opentrons/shared-data'
import type {
QuickTransferSetupState,
QuickTransferWizardAction,
} from './types'
import { LabwareFilter } from '../../pages/Labware/types'

interface SelectDestLabwareProps {
onNext: () => void
onBack: () => void
exitButtonProps: React.ComponentProps<typeof SmallButton>
state: QuickTransferSetupState
dispatch: React.Dispatch<QuickTransferWizardAction>
}

export function SelectDestLabware(
props: SelectDestLabwareProps
): JSX.Element | null {
const { onNext, onBack, exitButtonProps, state, dispatch } = props
const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
const labwareDisplayCategoryFilters: LabwareFilter[] = [
'all',
'wellPlate',
'reservoir',
]
if (state.pipette?.channels === 1) {
labwareDisplayCategoryFilters.push('tubeRack')
}
const [selectedCategory, setSelectedCategory] = React.useState<LabwareFilter>(
'all'
)
const [selectedLabware, setSelectedLabware] = React.useState<
LabwareDefinition2 | 'source' | undefined
>(state.destination)

if (state.pipette == null) return null

const compatibleLabwareDefinitions = getCompatibleLabwareByCategory(
state.pipette.channels,
selectedCategory
)

const handleClickNext = (): void => {
// the button will be disabled if this values is null
if (selectedLabware != null) {
dispatch({
type: 'SET_DEST_LABWARE',
labware: selectedLabware,
})
onNext()
}
}
return (
<Flex>
<ChildNavigation
header={t('select_dest_labware')}
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
onClickBack={onBack}
onClickButton={handleClickNext}
secondaryButtonProps={exitButtonProps}
top={SPACING.spacing8}
buttonIsDisabled={selectedLabware == null}
/>
<Flex
flexDirection={DIRECTION_COLUMN}
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
width="100%"
>
<Flex
gridGap={SPACING.spacing8}
height={SPACING.spacing80}
backgroundColor={COLORS.white}
width="100%"
flexDirection={DIRECTION_ROW}
position={POSITION_FIXED}
top={SPACING.spacing120}
marginBottom={SPACING.spacing24}
alignItems={ALIGN_CENTER}
>
{labwareDisplayCategoryFilters.map(category => (
<TabbedButton
key={category}
title={category}
isSelected={category === selectedCategory}
onClick={() => setSelectedCategory(category)}
height={SPACING.spacing60}
>
{t(category)}
</TabbedButton>
))}
</Flex>
<Flex
gridGap={SPACING.spacing4}
flexDirection={DIRECTION_COLUMN}
marginTop="175px"
>
{selectedCategory === 'all' && state?.source != null ? (
<LargeButton
buttonType={
selectedLabware === 'source' ? 'primary' : 'secondary'
}
onClick={() => {
setSelectedLabware('source')
}}
buttonText={t('source_labware')}
subtext={state.source.metadata.displayName}
/>
) : null}
{compatibleLabwareDefinitions?.map(definition => {
return definition.metadata.displayName != null ? (
<LargeButton
key={`${selectedCategory}-${definition.metadata.displayName}`}
buttonType={
selectedLabware !== 'source' &&
selectedLabware?.metadata.displayName ===
definition.metadata.displayName
? 'primary'
: 'secondary'
}
onClick={() => {
setSelectedLabware(definition)
}}
buttonText={definition.metadata.displayName}
/>
) : null
})}
</Flex>
</Flex>
</Flex>
)
}
59 changes: 59 additions & 0 deletions app/src/organisms/QuickTransferFlow/SelectDestWells.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof SmallButton>
state: QuickTransferSetupState
dispatch: React.Dispatch<QuickTransferWizardAction>
}

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 (
<Flex>
<ChildNavigation
header={t('select_dest_wells')}
onClickBack={onBack}
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
onClickButton={handleClickNext}
buttonIsDisabled={false}
secondaryButtonProps={exitButtonProps}
top={SPACING.spacing8}
/>
<Flex
marginTop={SPACING.spacing120}
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
>
TODO: Add destination well selection deck map
</Flex>
</Flex>
)
}
Loading

0 comments on commit 99445fb

Please sign in to comment.