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): orchestration component for new quick transfer flow #14808

Merged
merged 8 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(app, components): add create new quick transfer screen
  • Loading branch information
smb2268 committed Apr 15, 2024
commit c993da353bfe8c1ab7c626ad75160ded343503b8
2 changes: 1 addition & 1 deletion app/src/assets/localization/en/quick_transfer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"set_aspirate_volume": "Set aspirate volume",
"set_dispense_volume": "Set dispense volume",
"set_transfer_volume": "Set transfer volume",
"use_deck_slots": "Quick transfers use deck slots B2-2. These slots hold a tip rack, a source labware, and a destination labware. <block>Make sure that your deck configuration is up to date to avoid collisions.</block>",
"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",
"labware": "Labware",
"pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.",
Expand Down
74 changes: 74 additions & 0 deletions app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as React from 'react'
import { useTranslation, Trans } from 'react-i18next'
import {
Flex,
SPACING,
StyledText,
DeckConfigurator,
TYPOGRAPHY,
DIRECTION_COLUMN,
} from '@opentrons/components'
import { SmallButton } from '../../atoms/buttons'
import { useDeckConfigurationQuery } from '@opentrons/react-api-client'
import { ChildNavigation } from '../ChildNavigation'

interface CreateNewTransferProps {
onNext: () => void
exitButtonProps: React.ComponentProps<typeof SmallButton>
}

export function CreateNewTransfer(props: CreateNewTransferProps): JSX.Element {
const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
const deckConfig = useDeckConfigurationQuery().data ?? []
return (
<Flex>
<ChildNavigation
header={t('create_new_transfer')}
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
onClickButton={props.onNext}
secondaryButtonProps={props.exitButtonProps}
top={SPACING.spacing8}
/>
<Flex
marginTop={SPACING.spacing80}
flexDirection={DIRECTION_COLUMN}
padding={`0 ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
>
<Flex gridGap={SPACING.spacing16}>
<Flex
width="50%"
paddingTop={SPACING.spacing32}
marginTop={SPACING.spacing32}
flexDirection={DIRECTION_COLUMN}
>
<Trans
t={t}
i18nKey="use_deck_slots"
components={{
block: (
<StyledText
css={TYPOGRAPHY.level4HeaderRegular}
marginBottom={SPACING.spacing16}
/>
),
}}
/>
</Flex>
<Flex width="50%">
<DeckConfigurator
deckConfig={deckConfig}
readOnly
handleClickAdd={() => {}}
handleClickRemove={() => {}}
additionalStaticFixtures={[
{ location: 'cutoutB2', label: t('tip_rack') },
{ location: 'cutoutC2', label: t('labware') },
{ location: 'cutoutD2', label: t('labware') },
]}
/>
</Flex>
</Flex>
</Flex>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as React from 'react'
import { fireEvent, screen } from '@testing-library/react'
import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'
import { DeckConfigurator } from '@opentrons/components'

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

import type * as OpentronsComponents from '@opentrons/components'

vi.mock('@opentrons/components', async importOriginal => {
const actual = await importOriginal<typeof OpentronsComponents>()
return {
...actual,
DeckConfigurator: vi.fn(),
}
})
const render = (props: React.ComponentProps<typeof CreateNewTransfer>) => {
return renderWithProviders(<CreateNewTransfer {...props} />, {
i18nInstance: i18n,
})
}

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

beforeEach(() => {
props = {
onNext: vi.fn(),
exitButtonProps: {
buttonType: 'tertiaryLowLight',
buttonText: 'Exit',
onClick: vi.fn(),
},
}
})
afterEach(() => {
vi.resetAllMocks()
})

it('renders the create new transfer screen and header', () => {
render(props)
screen.getByText('Create new quick transfer')
screen.getByText(
'Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.'
)
screen.getByText(
'Make sure that your deck configuration is up to date to avoid collisions.'
)
expect(vi.mocked(DeckConfigurator)).toHaveBeenCalled()
})
it('renders exit and continue buttons and they work as expected', () => {
render(props)
const exitBtn = screen.getByText('Exit')
fireEvent.click(exitBtn)
expect(props.exitButtonProps.onClick).toHaveBeenCalled()
const continueBtn = screen.getByText('Continue')
fireEvent.click(continueBtn)
expect(props.onNext).toHaveBeenCalled()
})
})
103 changes: 59 additions & 44 deletions app/src/organisms/QuickTransferFlow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import * as React from 'react'
import { useHistory } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import {
Flex,
StepMeter,
SPACING,
DIRECTION_COLUMN,
} from '@opentrons/components'
import { Flex, StepMeter, SPACING } from '@opentrons/components'
import { SmallButton } from '../../atoms/buttons'
import { ChildNavigation } from '../ChildNavigation'
import { CreateNewTransfer } from './CreateNewTransfer'

import type {
QuickTransferSetupState,
Expand Down Expand Up @@ -86,32 +83,50 @@ export const QuickTransferFlow = (): JSX.Element => {
const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
// const [state, dispatch] = React.useReducer(reducer, initialQuickTransferState)
const [currentStep, setCurrentStep] = React.useState(1)
const [wizardHeader, setWizardHeader] = React.useState<string>(
t('create_new_transfer')
)
const [wizardHeader, setWizardHeader] = React.useState<string | null>(null)
const [continueIsDisabled] = React.useState<boolean>(false)
const wizardBody = <Flex>Wizard Body</Flex>
const [wizardBody, setWizardBody] = React.useState<JSX.Element>(<></>)
// every child component will take state as a prop, an anonymous
// dispatch function related to that step (except create new),
// and a function to disable the continue button

const exitButtonProps: React.ComponentProps<typeof SmallButton> = {
buttonType: 'tertiaryLowLight',
buttonText: i18n.format(t('shared:exit'), 'capitalize'),
onClick: () => {
history.push('protocols')
},
}
React.useEffect(() => {
if (currentStep === 1) {
setWizardHeader(t('create_new_transfer'))
setWizardHeader(null)
setWizardBody(
<CreateNewTransfer
onNext={() => setCurrentStep(prevStep => prevStep + 1)}
exitButtonProps={exitButtonProps}
/>
)
} else if (currentStep === 2) {
setWizardHeader(t('select_attached_pipette'))
setWizardBody(<></>)
} else if (currentStep === 3) {
setWizardHeader(t('select_tip_rack'))
setWizardBody(<></>)
} else if (currentStep === 4) {
setWizardHeader(t('select_source_labware'))
setWizardBody(<></>)
} else if (currentStep === 5) {
setWizardHeader(t('select_source_wells'))
setWizardBody(<></>)
} else if (currentStep === 6) {
setWizardHeader(t('select_dest_labware'))
setWizardBody(<></>)
} else if (currentStep === 7) {
setWizardHeader(t('select_dest_wells'))
setWizardBody(<></>)
} else if (currentStep === 8) {
setWizardHeader(t('set_transfer_volume'))
setWizardBody(<></>)
}
}, [currentStep])

Expand All @@ -121,40 +136,40 @@ export const QuickTransferFlow = (): JSX.Element => {
totalSteps={QUICK_TRANSFER_WIZARD_STEPS}
currentStep={currentStep}
/>
<Flex
marginTop={SPACING.spacing8}
padding={`${SPACING.spacing32} ${SPACING.spacing40} ${SPACING.spacing40}`}
flexDirection={DIRECTION_COLUMN}
>
<ChildNavigation
header={wizardHeader}
onClickBack={
currentStep === 1
? undefined
: () => {
setCurrentStep(prevStep => prevStep - 1)
}
}
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
onClickButton={() => {
if (currentStep === 8) {
history.push('protocols')
} else {
setCurrentStep(prevStep => prevStep + 1)
{wizardHeader != null ? (
<Flex>
<ChildNavigation
header={wizardHeader}
onClickBack={
currentStep === 1
? undefined
: () => {
setCurrentStep(prevStep => prevStep - 1)
}
}
}}
buttonIsDisabled={continueIsDisabled}
secondaryButtonProps={{
buttonType: 'tertiaryLowLight',
buttonText: i18n.format(t('shared:exit'), 'capitalize'),
onClick: () => {
history.push('protocols')
},
}}
top={SPACING.spacing8}
/>
<Flex marginTop={SPACING.spacing80}>{wizardBody}</Flex>
</Flex>
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
onClickButton={() => {
if (currentStep === 8) {
history.push('protocols')
} else {
setCurrentStep(prevStep => prevStep + 1)
}
}}
buttonIsDisabled={continueIsDisabled}
secondaryButtonProps={{
buttonType: 'tertiaryLowLight',
buttonText: i18n.format(t('shared:exit'), 'capitalize'),
onClick: () => {
history.push('protocols')
},
}}
top={SPACING.spacing8}
/>
<Flex marginTop={SPACING.spacing80}>{wizardBody}</Flex>
</Flex>
) : (
wizardBody
)}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ const deckConfig: CutoutConfig[] = [
},
]

const staticFixtures = [
{ location: 'cutoutB2', label: 'Tip rack' },
{ location: 'cutoutC2', label: 'Labware' },
{ location: 'cutoutD2', label: 'Labware' },
]

export const Default = Template.bind({})
Default.args = {
deckConfig,
Expand All @@ -85,3 +91,12 @@ ReadOnly.args = {
handleClickRemove: cutoutId => console.log(`remove at ${cutoutId}`),
readOnly: true,
}

export const ReadOnlyWithStaticFixtures = Template.bind({})
ReadOnlyWithStaticFixtures.args = {
deckConfig,
handleClickAdd: () => {},
handleClickRemove: () => {},
readOnly: true,
additionalStaticFixtures: staticFixtures,
}
56 changes: 56 additions & 0 deletions components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react'

import { Btn, Text } from '../../primitives'
import { TYPOGRAPHY } from '../../ui-style-constants'
import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject'
import {
CONFIG_STYLE_READ_ONLY,
FIXTURE_HEIGHT,
MIDDLE_SLOT_FIXTURE_WIDTH,
Y_ADJUSTMENT,
COLUMN_2_X_ADJUSTMENT,
} from './constants'

import type { CutoutId, DeckDefinition } from '@opentrons/shared-data'

interface StaticFixtureProps {
deckDefinition: DeckDefinition
fixtureLocation: CutoutId
label: string
}

/**
* this component allows us to add static labeled fixtures to the center column of a deck
* config map
*/

export function StaticFixture(props: StaticFixtureProps): JSX.Element {
const { deckDefinition, fixtureLocation, label } = props

const staticCutout = deckDefinition.locations.cutouts.find(
cutout => cutout.id === fixtureLocation
)

/**
* deck definition cutout position is the position of the single slot located within that cutout
* so, to get the position of the cutout itself we must add an adjustment to the slot position
*/
const [xSlotPosition = 0, ySlotPosition = 0] = staticCutout?.position ?? []
const y = ySlotPosition + Y_ADJUSTMENT
let x = xSlotPosition + COLUMN_2_X_ADJUSTMENT

return (
<RobotCoordsForeignObject
width={MIDDLE_SLOT_FIXTURE_WIDTH}
height={FIXTURE_HEIGHT}
x={x}
y={y}
flexProps={{ flex: '1' }}
foreignObjectProps={{ flex: '1' }}
>
<Btn css={CONFIG_STYLE_READ_ONLY} cursor={'default'} onClick={() => {}}>
<Text css={TYPOGRAPHY.smallBodyTextSemiBold}>{label}</Text>
</Btn>
</RobotCoordsForeignObject>
)
}
2 changes: 2 additions & 0 deletions components/src/hardware-sim/DeckConfigurator/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import { RESPONSIVENESS, SPACING } from '../../ui-style-constants'
* Position is relative to deck definition slot positions and a custom stroke applied to the single slot fixture SVG
*/
export const FIXTURE_HEIGHT = 102.0
export const MIDDLE_SLOT_FIXTURE_WIDTH = 158.5
export const SINGLE_SLOT_FIXTURE_WIDTH = 243.5
export const STAGING_AREA_FIXTURE_WIDTH = 314.5

export const COLUMN_1_X_ADJUSTMENT = -100
export const COLUMN_2_X_ADJUSTMENT = -15.5
export const COLUMN_3_X_ADJUSTMENT = -15.5
export const Y_ADJUSTMENT = -8

Expand Down
Loading