From b42927a74661a0264dc33d4aa0d2ca6cc63f5d87 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Mon, 22 Apr 2024 11:31:02 -0400 Subject: [PATCH] feat(app): add tiprack selection step to quick transfer flow (#14950) fix PLAT-290 --- .../QuickTransferFlow/SelectPipette.tsx | 7 +- .../QuickTransferFlow/SelectTipRack.tsx | 80 +++++++++++++++++ .../__tests__/SelectTipRack.test.tsx | 86 +++++++++++++++++++ app/src/organisms/QuickTransferFlow/index.tsx | 20 ++++- app/src/organisms/QuickTransferFlow/types.ts | 14 +-- components/src/atoms/StepMeter/index.tsx | 14 ++- 6 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 app/src/organisms/QuickTransferFlow/SelectTipRack.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/SelectTipRack.test.tsx diff --git a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx index 0f92ca0d508..6ef31157fdf 100644 --- a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx @@ -79,9 +79,12 @@ export function SelectPipette(props: SelectPipetteProps): JSX.Element { marginTop={SPACING.spacing120} flexDirection={DIRECTION_COLUMN} padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`} - gridGap={SPACING.spacing16} + gridGap={SPACING.spacing4} > - + {t('pipette_currently_attached')} {leftPipetteSpecs != null ? ( diff --git a/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx b/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx new file mode 100644 index 00000000000..bed59baa54b --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, SPACING, DIRECTION_COLUMN } from '@opentrons/components' +import { getAllDefinitions } from '@opentrons/shared-data' +import { SmallButton, LargeButton } from '../../atoms/buttons' +import { ChildNavigation } from '../ChildNavigation' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { + QuickTransferSetupState, + QuickTransferWizardAction, +} from './types' + +interface SelectTipRackProps { + onNext: () => void + onBack: () => void + exitButtonProps: React.ComponentProps + state: QuickTransferSetupState + dispatch: React.Dispatch +} + +export function SelectTipRack(props: SelectTipRackProps): JSX.Element { + const { onNext, onBack, exitButtonProps, state, dispatch } = props + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + + const allLabwareDefinitionsByUri = getAllDefinitions() + const selectedPipetteDefaultTipracks = + state.pipette?.liquids.default.defaultTipracks ?? [] + + const [selectedTipRack, setSelectedTipRack] = React.useState< + LabwareDefinition2 | undefined + >(state.tipRack) + + const handleClickNext = (): void => { + // the button will be disabled if this values is null + if (selectedTipRack != null) { + dispatch({ + type: 'SELECT_TIP_RACK', + tipRack: selectedTipRack, + }) + onNext() + } + } + return ( + + + + {selectedPipetteDefaultTipracks.map(tipRack => { + const tipRackDef = allLabwareDefinitionsByUri[tipRack] + + return tipRackDef != null ? ( + { + setSelectedTipRack(tipRackDef) + }} + buttonText={tipRackDef.metadata.displayName} + /> + ) : null + })} + + + ) +} diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectTipRack.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectTipRack.test.tsx new file mode 100644 index 00000000000..b32b3188910 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectTipRack.test.tsx @@ -0,0 +1,86 @@ +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 { SelectTipRack } from '../SelectTipRack' + +vi.mock('@opentrons/react-api-client') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('SelectTipRack', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onNext: vi.fn(), + onBack: vi.fn(), + exitButtonProps: { + buttonType: 'tertiaryLowLight', + buttonText: 'Exit', + onClick: vi.fn(), + }, + state: { + mount: 'left', + pipette: { + liquids: { + default: { + defaultTipracks: [ + 'opentrons/opentrons_flex_96_tiprack_1000ul/1', + 'opentrons/opentrons_flex_96_tiprack_200ul/1', + 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_1000ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_200ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + ], + }, + }, + } as any, + }, + dispatch: vi.fn(), + } + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the select tip rack screen, header, and exit button', () => { + render(props) + screen.getByText('Select tip rack') + const exitBtn = screen.getByText('Exit') + fireEvent.click(exitBtn) + expect(props.exitButtonProps.onClick).toHaveBeenCalled() + }) + + it('renders continue button and it is disabled if no tip rack is selected', () => { + render(props) + screen.getByText('Continue') + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeDisabled() + }) + + it('selects tip rack by default if there is one in state, button will be enabled', () => { + render({ ...props, state: { tipRack: { def: 'definition' } as any } }) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeEnabled() + fireEvent.click(continueBtn) + expect(props.onNext).toHaveBeenCalled() + }) + + it('enables continue button if you click a tip rack', () => { + render(props) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeDisabled() + const tipRackButton = screen.getByText('Opentrons Flex 96 Tip Rack 200 µL') + fireEvent.click(tipRackButton) + expect(continueBtn).toBeEnabled() + fireEvent.click(continueBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(props.onNext).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/index.tsx b/app/src/organisms/QuickTransferFlow/index.tsx index 36d0175b0db..cdfecc4fbe2 100644 --- a/app/src/organisms/QuickTransferFlow/index.tsx +++ b/app/src/organisms/QuickTransferFlow/index.tsx @@ -1,11 +1,17 @@ import * as React from 'react' import { useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { Flex, StepMeter, SPACING } from '@opentrons/components' +import { + Flex, + StepMeter, + SPACING, + POSITION_STICKY, +} from '@opentrons/components' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '../ChildNavigation' import { CreateNewTransfer } from './CreateNewTransfer' import { SelectPipette } from './SelectPipette' +import { SelectTipRack } from './SelectTipRack' import { quickTransferReducer } from './utils' import type { QuickTransferSetupState } from './types' @@ -66,6 +72,16 @@ export const QuickTransferFlow = (): JSX.Element => { exitButtonProps={exitButtonProps} /> ) + } else if (currentStep === 3) { + modalContent = ( + setCurrentStep(prevStep => prevStep - 1)} + onNext={() => setCurrentStep(prevStep => prevStep + 1)} + exitButtonProps={exitButtonProps} + /> + ) } else { modalContent = null } @@ -76,6 +92,8 @@ export const QuickTransferFlow = (): JSX.Element => { {modalContent == null ? ( diff --git a/app/src/organisms/QuickTransferFlow/types.ts b/app/src/organisms/QuickTransferFlow/types.ts index 814dae22a71..1d43017a58c 100644 --- a/app/src/organisms/QuickTransferFlow/types.ts +++ b/app/src/organisms/QuickTransferFlow/types.ts @@ -1,14 +1,14 @@ import { ACTIONS } from './constants' import type { Mount } from '@opentrons/api-client' -import type { LabwareDefinition1, PipetteV2Specs } from '@opentrons/shared-data' +import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data' export interface QuickTransferSetupState { pipette?: PipetteV2Specs mount?: Mount - tipRack?: LabwareDefinition1 - source?: LabwareDefinition1 + tipRack?: LabwareDefinition2 + source?: LabwareDefinition2 sourceWells?: string[] - destination?: LabwareDefinition1 + destination?: LabwareDefinition2 destinationWells?: string[] volume?: number } @@ -29,11 +29,11 @@ interface SelectPipetteAction { } interface SelectTipRackAction { type: typeof ACTIONS.SELECT_TIP_RACK - tipRack: LabwareDefinition1 + tipRack: LabwareDefinition2 } interface SetSourceLabwareAction { type: typeof ACTIONS.SET_SOURCE_LABWARE - labware: LabwareDefinition1 + labware: LabwareDefinition2 } interface SetSourceWellsAction { type: typeof ACTIONS.SET_SOURCE_WELLS @@ -41,7 +41,7 @@ interface SetSourceWellsAction { } interface SetDestLabwareAction { type: typeof ACTIONS.SET_DEST_LABWARE - labware: LabwareDefinition1 + labware: LabwareDefinition2 } interface SetDestWellsAction { type: typeof ACTIONS.SET_DEST_WELLS diff --git a/components/src/atoms/StepMeter/index.tsx b/components/src/atoms/StepMeter/index.tsx index 14bbf48c6ca..91f151fb5c9 100644 --- a/components/src/atoms/StepMeter/index.tsx +++ b/components/src/atoms/StepMeter/index.tsx @@ -5,13 +5,15 @@ import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' import { COLORS } from '../../helix-design-system' import { POSITION_ABSOLUTE, POSITION_RELATIVE } from '../../styles' -interface StepMeterProps { +import type { StyleProps } from '../../primitives' + +interface StepMeterProps extends StyleProps { totalSteps: number currentStep: number | null } export const StepMeter = (props: StepMeterProps): JSX.Element => { - const { totalSteps, currentStep } = props + const { totalSteps, currentStep, ...styleProps } = props const progress = currentStep != null ? currentStep : 0 const percentComplete = `${ // this logic puts a cap at 100% percentComplete which we should never run into @@ -21,7 +23,7 @@ export const StepMeter = (props: StepMeterProps): JSX.Element => { }%` const StepMeterContainer = css` - position: ${POSITION_RELATIVE}; + position: ${styleProps.position ? styleProps.position : POSITION_RELATIVE}; height: ${SPACING.spacing4}; background-color: ${COLORS.grey30}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { @@ -41,7 +43,11 @@ export const StepMeter = (props: StepMeterProps): JSX.Element => { ` return ( - + )