Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app): add labware selection and volume entry screens #15074

Merged
merged 10 commits into from
May 3, 2024
9 changes: 9 additions & 0 deletions app/src/assets/localization/en/quick_transfer.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"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",
"select_dest_labware": "Select destination labware",
"select_dest_wells": "Select destination wells",
Expand All @@ -12,10 +16,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)."
}
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Designs for this component show no rounded edges, the default for the keyboard component we use is border-radius 5

background-color: #cbcccc; /* grey35 */
font-family: 'Public Sans', sans-serif;
padding: 8px;
Expand Down
146 changes: 146 additions & 0 deletions app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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'
)
if (state.pipette == null) return null
const compatibleLabwareDefinitions = getCompatibleLabwareByCategory(
state.pipette.channels,
selectedCategory
)

const [selectedLabware, setSelectedLabware] = React.useState<

Check failure on line 54 in app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx

View workflow job for this annotation

GitHub Actions / js checks

React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render

Check failure on line 54 in app/src/organisms/QuickTransferFlow/SelectDestLabware.tsx

View workflow job for this annotation

GitHub Actions / js checks

React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render
LabwareDefinition2 | 'source' | undefined
>(state.destination)

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) {

Check failure on line 32 in app/src/organisms/QuickTransferFlow/SelectDestWells.tsx

View workflow job for this annotation

GitHub Actions / js checks

Expected '!==' and instead saw '!='

Check failure on line 32 in app/src/organisms/QuickTransferFlow/SelectDestWells.tsx

View workflow job for this annotation

GitHub Actions / js checks

Expected '!==' and instead saw '!='
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>
)
}
1 change: 1 addition & 0 deletions app/src/organisms/QuickTransferFlow/SelectPipette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { getPipetteSpecsV2, RIGHT, LEFT } from '@opentrons/shared-data'
import { SmallButton, LargeButton } from '../../atoms/buttons'
import { ChildNavigation } from '../ChildNavigation'
import { generateCompatibleLabwareForPipette } from './utils'

Check failure on line 14 in app/src/organisms/QuickTransferFlow/SelectPipette.tsx

View workflow job for this annotation

GitHub Actions / js checks

'generateCompatibleLabwareForPipette' is defined but never used

Check failure on line 14 in app/src/organisms/QuickTransferFlow/SelectPipette.tsx

View workflow job for this annotation

GitHub Actions / js checks

'generateCompatibleLabwareForPipette' is defined but never used

import type { PipetteData, Mount } from '@opentrons/api-client'
import type {
Expand Down
134 changes: 134 additions & 0 deletions app/src/organisms/QuickTransferFlow/SelectSourceLabware.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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 SelectSourceLabwareProps {
onNext: () => void
onBack: () => void
exitButtonProps: React.ComponentProps<typeof SmallButton>
state: QuickTransferSetupState
dispatch: React.Dispatch<QuickTransferWizardAction>
}

export function SelectSourceLabware(
props: SelectSourceLabwareProps
): 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'
)
if (state.pipette == null) return null

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

const [selectedLabware, setSelectedLabware] = React.useState<

Check failure on line 55 in app/src/organisms/QuickTransferFlow/SelectSourceLabware.tsx

View workflow job for this annotation

GitHub Actions / js checks

React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render

Check failure on line 55 in app/src/organisms/QuickTransferFlow/SelectSourceLabware.tsx

View workflow job for this annotation

GitHub Actions / js checks

React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render
LabwareDefinition2 | undefined
>(state.source)

const handleClickNext = (): void => {
// the button will be disabled if this values is null
if (selectedLabware != null) {
dispatch({
type: 'SET_SOURCE_LABWARE',
labware: selectedLabware,
})
onNext()
}
}
return (
<Flex>
<ChildNavigation
header={t('select_source_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"
>
{compatibleLabwareDefinitions?.map(definition => {
return definition.metadata.displayName != null ? (
<LargeButton
key={`${selectedCategory}-${definition.metadata.displayName}`}
buttonType={
selectedLabware?.metadata.displayName ===
definition.metadata.displayName
? 'primary'
: 'secondary'
}
onClick={() => {
setSelectedLabware(definition)
}}
buttonText={definition.metadata.displayName}
/>
) : null
})}
</Flex>
</Flex>
</Flex>
)
}
Loading
Loading