Skip to content

Commit

Permalink
feat(components, protocol-designer): add Thermocycler step details UI
Browse files Browse the repository at this point in the history
Similar to viewing substeps detail for moveLiquid type steps, this PR adds UI for thermocycler
substep details. Also, I move the step details toolbox to the right side of the screen as designs
specify, and make a few other small styling changes to our ListItem, Toolbox, and StepOverFlowMenu
components.

Closes AUTH-882
  • Loading branch information
ncdiehl11 committed Oct 22, 2024
1 parent b2f2546 commit d160c3e
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 15 deletions.
1 change: 0 additions & 1 deletion components/src/atoms/ListItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export function ListItem(props: ListItemProps): JSX.Element {
background-color: ${listItemProps.backgroundColor};
width: 100%;
height: ${FLEX_MAX_CONTENT};
padding: 0;
border-radius: ${BORDERS.borderRadius4};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
Expand Down
2 changes: 1 addition & 1 deletion components/src/organisms/Toolbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function Toolbox(props: ToolboxProps): JSX.Element {
height={height}
{...positionStyles}
borderRadius={BORDERS.borderRadius8}
minWidth="19.5rem"
width={width}
flex="0"
>
<Flex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"closed": "closed",
"open": "open"
},
"substep_settings": "<text>Set block temperature to</text><tagTemperature/><text>for</text><tagDuration/>",
"thermocycler_profile": {
"end_hold": {
"block": "<text>End at thermocycler block</text><tag/>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
const isPipetteStep =
savedStepFormData.stepType === 'moveLiquid' ||
savedStepFormData.stepType === 'mix'
const isThermocyclerStep = savedStepFormData.stepType === 'thermocycler'
const isThermocyclerProfile =
savedStepFormData.thermocyclerFormType === 'thermocyclerProfile'

return (
<>
Expand Down Expand Up @@ -183,7 +184,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
)}
<Flex
ref={menuRootRef}
zIndex={5}
zIndex={12}
top={top}
left="19.5rem"
position={POSITION_ABSOLUTE}
Expand Down Expand Up @@ -211,9 +212,10 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element {
{formData != null ? null : (
<MenuButton onClick={confirm}>{t('edit_step')}</MenuButton>
)}
{isPipetteStep || isThermocyclerStep ? (
{isPipetteStep || isThermocyclerProfile ? (
<MenuButton
onClick={() => {
setStepOverflowMenu(false)
dispatch(hoverOnStep(stepId))
dispatch(toggleViewSubstep(stepId))
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,14 @@ export function SubstepsToolbox(
return null
}

const uiStepType = t(`application:stepType.${formData.stepType}`)

return ('commandCreatorFnName' in substeps &&
(substeps.commandCreatorFnName === 'transfer' ||
substeps.commandCreatorFnName === 'consolidate' ||
substeps.commandCreatorFnName === 'distribute' ||
substeps.commandCreatorFnName === 'mix')) ||
substeps.substepType === THERMOCYCLER_PROFILE ? (
<Toolbox
width="396px"
width="max-content"
childrenPadding="0"
confirmButton={
<PrimaryButton
Expand All @@ -72,15 +70,15 @@ export function SubstepsToolbox(
title={
<StyledText desktopStyle="bodyLargeSemiBold">
{i18n.format(
t(`protocol_steps:step_substeps`, { stepType: uiStepType }),
t(`protocol_steps:step_substeps`, { stepType: formData.stepName }),
'capitalize'
)}
</StyledText>
}
>
<Flex padding={SPACING.spacing12} width="100%">
<Flex padding={SPACING.spacing12}>
{substeps.substepType === THERMOCYCLER_PROFILE ? (
<ThermocyclerProfileSubsteps />
<ThermocyclerProfileSubsteps key="substeps" stepId={stepId} />
) : (
<PipettingSubsteps
key="substeps"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,111 @@
export function ThermocyclerProfileSubsteps(): JSX.Element {
return <div>Wire this up</div>
import { useSelector } from 'react-redux'
import { Trans, useTranslation } from 'react-i18next'
import {
ALIGN_CENTER,
ALIGN_FLEX_END,
DIRECTION_COLUMN,
FLEX_MAX_CONTENT,
Flex,
ListItem,
SPACING,
StyledText,
Tag,
} from '@opentrons/components'
import { getSavedStepForms } from '../../../../step-forms/selectors'

import type { ProfileStepItem } from '../../../../form-types'
import type { ThermocyclerCycleType } from '../StepForm/StepTools/ThermocyclerTools/ThermocyclerCycle'
import type { ThermocyclerStepType } from '../StepForm/StepTools/ThermocyclerTools/ThermocyclerStep'

interface ThermocyclerProfileSubstepsProps {
stepId: string
}
export function ThermocyclerProfileSubsteps(
props: ThermocyclerProfileSubstepsProps
): JSX.Element {
const { stepId } = props

const savedStepForms = useSelector(getSavedStepForms)
const step = savedStepForms[stepId]
const orderedSubsteps = step.orderedProfileItems.map(
(id: string) => step.profileItemsById[id]
)

return (
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
width={FLEX_MAX_CONTENT}
>
{orderedSubsteps.map(
(substep: ThermocyclerStepType | ThermocyclerCycleType) => {
const content =
substep.type === 'profileCycle' ? (
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing12}
>
{substep.steps.map((profileStep: ProfileStepItem) => {
const {
temperature,
durationMinutes,
durationSeconds,
} = profileStep
return (
<ThermocyclerSubstep

Check failure on line 55 in protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ThermocyclerProfileSubsteps.tsx

View workflow job for this annotation

GitHub Actions / js checks

Missing "key" prop for element in iterator

Check failure on line 55 in protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ThermocyclerProfileSubsteps.tsx

View workflow job for this annotation

GitHub Actions / js checks

Missing "key" prop for element in iterator
temperature={temperature}
duration={`${durationMinutes}:${durationSeconds}`}
/>
)
})}
<StyledText
desktopStyle="bodyDefaultRegular"
alignSelf={ALIGN_FLEX_END}
>
{`Repeat ${substep.repetitions} times `}
</StyledText>
</Flex>
) : (
<ThermocyclerSubstep
temperature={substep.temperature}
duration={`${substep.durationMinutes}:${substep.durationSeconds}`}
/>
)
return (
<ListItem type="noActive" width="100%" padding={SPACING.spacing12}>

Check failure on line 75 in protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ThermocyclerProfileSubsteps.tsx

View workflow job for this annotation

GitHub Actions / js checks

Missing "key" prop for element in iterator

Check failure on line 75 in protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ThermocyclerProfileSubsteps.tsx

View workflow job for this annotation

GitHub Actions / js checks

Missing "key" prop for element in iterator
{content}
</ListItem>
)
}
)}
</Flex>
)
}

interface ThermocyclerSubstepProps {
temperature: string
duration: string
}

function ThermocyclerSubstep(props: ThermocyclerSubstepProps) {

Check failure on line 90 in protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ThermocyclerProfileSubsteps.tsx

View workflow job for this annotation

GitHub Actions / js checks

Missing return type on function

Check failure on line 90 in protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ThermocyclerProfileSubsteps.tsx

View workflow job for this annotation

GitHub Actions / js checks

Missing return type on function
const { temperature, duration } = props
const { t } = useTranslation(['application', 'protocol_steps'])
return (
<Flex gridGap={SPACING.spacing4} alignItems={ALIGN_CENTER}>
<Trans
t={t}
i18nKey="protocol_steps:thermocycler_module.substep_settings"
components={{
text: <StyledText desktopStyle="bodyDefaultRegular" />,
tagTemperature: (
<Tag
type="default"
text={`${temperature}${t('application:units.degrees')}`}
/>
),
tagDuration: <Tag type="default" text={duration} />,
}}
/>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, vi, beforeEach, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithProviders } from '../../../../../__testing-utils__'
import { i18n } from '../../../../../assets/localization'
import { getSavedStepForms } from '../../../../../step-forms/selectors'
import { ThermocyclerProfileSubsteps } from '../ThermocyclerProfileSubsteps'
import type { FormData } from '../../../../../form-types'

const render = (
props: React.ComponentProps<typeof ThermocyclerProfileSubsteps>
) => {
return renderWithProviders(<ThermocyclerProfileSubsteps {...props} />, {
i18nInstance: i18n,
})[0]
}
vi.mock('../../../../../step-forms/selectors')
const THERMOCYCLER_STEP_ID = 'tcStep123'
const MOCK_THERMOCYCLER_ORDERED_SUBSTEP_IDS = [
'292b0d70-fa06-4ab1-adc9-f26c589babf4',
'0965e0de-2d01-4e4e-8fb3-1e66306fe7e5',
]
const MOCK_THERMOCYCLER_SUBSTEP_ITEMS = {
'292b0d70-fa06-4ab1-adc9-f26c589babf4': {
id: '292b0d70-fa06-4ab1-adc9-f26c589babf4',
title: '',
steps: [
{
durationMinutes: '00',
durationSeconds: '30',
id: 'f90cc374-2eeb-4205-80c6-63c5c77215a5',
temperature: '10',
title: 'cyclestep1',
type: 'profileStep',
},
{
durationMinutes: '1',
durationSeconds: '30',
id: '462b3d8f-bb8a-4e11-ae98-8f1d46e8507e',
temperature: '55',
title: 'cyclestep2',
type: 'profileStep',
},
],
type: 'profileCycle',
repetitions: '28',
},
'0965e0de-2d01-4e4e-8fb3-1e66306fe7e5': {
durationMinutes: '5',
durationSeconds: '00',
id: '0965e0de-2d01-4e4e-8fb3-1e66306fe7e5',
temperature: '39',
title: 'last step',
type: 'profileStep',
},
}

describe('TimelineToolbox', () => {
let props: React.ComponentProps<typeof ThermocyclerProfileSubsteps>
beforeEach(() => {
props = { stepId: THERMOCYCLER_STEP_ID }
vi.mocked(getSavedStepForms).mockReturnValue({
[THERMOCYCLER_STEP_ID]: ({
orderedProfileItems: MOCK_THERMOCYCLER_ORDERED_SUBSTEP_IDS,
profileItemsById: MOCK_THERMOCYCLER_SUBSTEP_ITEMS,
} as unknown) as FormData,
})
})
it('renders all profile steps, including cycles and steps', () => {
render(props)
expect(screen.getAllByText('Set block temperature to').length === 3)
screen.getByText('10°C')
screen.getByText('55°C')
screen.getByText('39°C')
screen.getByText('00:30')
screen.getByText('1:30')
screen.getByText('5:00')
})
})
5 changes: 3 additions & 2 deletions protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ export function ProtocolSteps(): JSX.Element {
gridGap={SPACING.spacing16}
height="calc(100vh - 4rem)"
justifyContent={JUSTIFY_SPACE_BETWEEN}
padding={SPACING.spacing12}
>
<TimelineToolbox />
{selectedSubstep ? <SubstepsToolbox stepId={selectedSubstep} /> : null}
<Flex
alignItems={ALIGN_CENTER}
flexDirection={DIRECTION_COLUMN}
Expand Down Expand Up @@ -118,7 +118,7 @@ export function ProtocolSteps(): JSX.Element {
</Flex>
</Flex>
{enableHoyKeyDisplay ? (
<Box position={POSITION_FIXED} left="20.25rem" bottom="0.75rem">
<Box position={POSITION_FIXED} left="21rem" bottom="0.75rem">
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
<Tag text={t('double_click_to_edit')} type="default" />
<Tag text={t('shift_click_to_select_all')} type="default" />
Expand All @@ -127,6 +127,7 @@ export function ProtocolSteps(): JSX.Element {
</Box>
) : null}
</Flex>
{selectedSubstep ? <SubstepsToolbox stepId={selectedSubstep} /> : null}
<StepForm />
{isMultiSelectMode ? <BatchEditToolbox /> : null}
</Flex>
Expand Down

0 comments on commit d160c3e

Please sign in to comment.