Skip to content

Commit

Permalink
feat(app): add labware selection and volume entry screens
Browse files Browse the repository at this point in the history
  • Loading branch information
smb2268 committed May 2, 2024
1 parent 077e337 commit 97f698e
Show file tree
Hide file tree
Showing 12 changed files with 730 additions and 60 deletions.
4 changes: 4 additions & 0 deletions app/src/assets/localization/en/quick_transfer.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"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",
Expand All @@ -18,6 +20,8 @@
"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",
Expand Down
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
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 '!='
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>
)
}
54 changes: 54 additions & 0 deletions app/src/organisms/QuickTransferFlow/SelectSourceWells.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 { Flex, SPACING } from '@opentrons/components'

import { SmallButton } from '../../atoms/buttons'
import { ChildNavigation } from '../ChildNavigation'

import type {
QuickTransferSetupState,
QuickTransferWizardAction,
} from './types'

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

export function SelectSourceWells(props: SelectSourceWellsProps): 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.source?.wells != null) {
dispatch({
type: 'SET_SOURCE_WELLS',
wells: Object.keys(state.source.wells),
})
onNext()
}
}
return (
<Flex>
<ChildNavigation
header={t('select_source_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 source well selection deck map
</Flex>
</Flex>
)
}
1 change: 0 additions & 1 deletion app/src/organisms/QuickTransferFlow/SelectTipRack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export function SelectTipRack(props: SelectTipRackProps): JSX.Element {
const handleClickNext = (): void => {
// the button will be disabled if this values is null
if (selectedTipRack != null) {
console.log('submitting tip rack')
dispatch({
type: 'SELECT_TIP_RACK',
tipRack: selectedTipRack,
Expand Down
125 changes: 125 additions & 0 deletions app/src/organisms/QuickTransferFlow/VolumeEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import {
Flex,
SPACING,
DIRECTION_COLUMN,
ALIGN_CENTER,
} from '@opentrons/components'

import { SmallButton } from '../../atoms/buttons'
import { ChildNavigation } from '../ChildNavigation'
import { InputField } from '../../atoms/InputField'
import { NumericalKeyboard } from '../../atoms/SoftwareKeyboard'
import { getVolumeLimits } from './utils'

import type {
QuickTransferSetupState,
QuickTransferWizardAction,
} from './types'

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

export function VolumeEntry(props: VolumeEntryProps): JSX.Element {
const { onNext, onBack, exitButtonProps, state, dispatch } = props
const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
const keyboardRef = React.useRef(null)

const [volume, setVolume] = React.useState<string>('')
const volumeRange = getVolumeLimits(state)
let headerCopy = t('set_transfer_volume')
let textEntryCopy = t('volume_per_well')
if (
state.sourceWells != null &&
state.destinationWells != null &&
state.sourceWells.length > state.destinationWells?.length
) {
headerCopy = t('set_aspirate_volume')
textEntryCopy = t('aspirate_volume')
} else if (
state.sourceWells != null &&
state.destinationWells != null &&
state.sourceWells.length < state.destinationWells.length
) {
headerCopy = t('set_dispense_volume')
textEntryCopy = t('dispense_volume')
}

const volumeAsNumber = Number(volume)

const handleClickNext = (): void => {
// the button will be disabled if this values is null
if (volumeAsNumber != null) {
dispatch({
type: 'SET_VOLUME',
volume: volumeAsNumber,
})
onNext()
}
}

const error =
volume != '' &&

Check failure on line 68 in app/src/organisms/QuickTransferFlow/VolumeEntry.tsx

View workflow job for this annotation

GitHub Actions / js checks

Expected '!==' and instead saw '!='
(volumeAsNumber < volumeRange.min || volumeAsNumber > volumeRange.max)
? t(`value_out_of_range`, {
min: volumeRange.min,
max: volumeRange.max,
})
: null

return (
<Flex>
<ChildNavigation
header={headerCopy}
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
onClickBack={onBack}
onClickButton={handleClickNext}
secondaryButtonProps={exitButtonProps}
top={SPACING.spacing8}
buttonIsDisabled={error != null || volume == ''}

Check failure on line 85 in app/src/organisms/QuickTransferFlow/VolumeEntry.tsx

View workflow job for this annotation

GitHub Actions / js checks

Expected '===' and instead saw '=='
/>
<Flex
alignSelf={ALIGN_CENTER}
gridGap={SPACING.spacing48}
paddingX={SPACING.spacing40}
padding={`${SPACING.spacing16} ${SPACING.spacing40} ${SPACING.spacing40}`}
marginTop="7.75rem" // using margin rather than justify due to content moving with error message
alignItems={ALIGN_CENTER}
height="22rem"
>
<Flex
width="30.5rem"
height="100%"
gridGap={SPACING.spacing24}
flexDirection={DIRECTION_COLUMN}
marginTop={SPACING.spacing68}
>
<InputField
type="text"
value={volume}
title={textEntryCopy}
error={error}
readOnly
/>
</Flex>
<Flex
paddingX={SPACING.spacing24}
height="21.25rem"
marginTop="7.75rem"
borderRadius="0"
>
<NumericalKeyboard
keyboardRef={keyboardRef}
onChange={e => setVolume(e)}
/>
</Flex>
</Flex>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as React from 'react'
import { fireEvent, screen } from '@testing-library/react'
import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'

import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'
import { SelectDestLabware } from '../SelectDestLabware'

vi.mock('@opentrons/react-api-client')
const render = (props: React.ComponentProps<typeof SelectDestLabware>) => {
return renderWithProviders(<SelectDestLabware {...props} />, {
i18nInstance: i18n,
})
}

describe('SelectDestLabware', () => {
let props: React.ComponentProps<typeof SelectDestLabware>

beforeEach(() => {
props = {
onNext: vi.fn(),
onBack: vi.fn(),
exitButtonProps: {
buttonType: 'tertiaryLowLight',
buttonText: 'Exit',
onClick: vi.fn(),
},
state: {
mount: 'left',
pipette: {
channels: 1,
} as any,
},
dispatch: vi.fn(),
}
})
afterEach(() => {
vi.resetAllMocks()
})

it('renders the select destination labware screen, header, and exit button', () => {
render(props)
screen.getByText('Select destination labware')
const exitBtn = screen.getByText('Exit')
fireEvent.click(exitBtn)
expect(props.exitButtonProps.onClick).toHaveBeenCalled()
})

it('renders continue button and it is disabled if no labware is selected', () => {
render(props)
screen.getByText('Continue')
const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
expect(continueBtn).toBeDisabled()
})

it('selects labware by default if there is one in state, button will be enabled', () => {
render({ ...props, state: { destination: { def: 'definition' } as any } })
const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
expect(continueBtn).toBeEnabled()
fireEvent.click(continueBtn)
expect(props.onNext).toHaveBeenCalled()
expect(props.dispatch).toHaveBeenCalled()
})

it('renders all categories for a single channel pipette', () => {
render(props)
screen.getByText('All labware')
screen.getByText('Well plates')
screen.getByText('Reservoirs')
screen.getByText('Tube racks')
})

it.fails('does not render tube rack tab for multi channel pipette', () => {
render({ ...props, state: { pipette: { channels: 8 } as any } })
screen.getByText('Tube racks')
})

it('renders the source labware as the first option', () => {
render({
...props,
state: {
source: { metadata: { displayName: 'source labware name' } } as any,
},
})
render(props)
screen.getByText('Source labware in D2')
screen.getByText('source labware name')
})
it('enables continue button if you select a labware', () => {
render({
...props,
state: {
source: { metadata: { displayName: 'source labware name' } } as any,
},
})
const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button')
expect(continueBtn).toBeDisabled()
const sourceLabware = screen.getByText('Source labware in D2')
fireEvent.click(sourceLabware)
expect(continueBtn).toBeEnabled()
})
})
Loading

0 comments on commit 97f698e

Please sign in to comment.