-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(app): add labware selection and volume entry screens
- Loading branch information
Showing
12 changed files
with
730 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 != '' && | ||
(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 == ''} | ||
/> | ||
<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> | ||
) | ||
} |
102 changes: 102 additions & 0 deletions
102
app/src/organisms/QuickTransferFlow/__tests__/SelectDestLabware.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |
Oops, something went wrong.